mirror of
				https://github.com/MichMich/MagicMirror.git
				synced 2025-10-31 02:36:47 +00:00 
			
		
		
		
	Compare commits
	
		
			1091 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 343e7de7bd | ||
|  | e87f50e64a | ||
|  | abe5c08a52 | ||
|  | f14e956166 | ||
|  | 2eaf9dfeeb | ||
|  | 0300ce05d5 | ||
|  | 9e0293047f | ||
|  | 298e585361 | ||
|  | 21a6d1bcea | ||
|  | bbe17b9b01 | ||
|  | 4381fc6695 | ||
|  | 818fd7b490 | ||
|  | 4c6c6f9ed3 | ||
|  | 2338a90191 | ||
|  | 6cad0e191b | ||
|  | f23e604ed4 | ||
|  | 0c1abad9df | ||
|  | fb96cc3c72 | ||
|  | e917f40542 | ||
|  | 29d467715f | ||
|  | b791a3761f | ||
|  | 02201d9f15 | ||
|  | b8dbf95497 | ||
|  | 65022f3ce1 | ||
|  | 44854d6a4f | ||
|  | 203014c654 | ||
|  | d3e53586fd | ||
|  | 9dd343054e | ||
|  | 11d17dd2c0 | ||
|  | 4cc3e481cc | ||
|  | 174da38cc8 | ||
|  | 0a35505e8d | ||
|  | 032f7ac299 | ||
|  | ca906c4b36 | ||
|  | 50f72f09ac | ||
|  | 02cf9b37e2 | ||
|  | 6f273d76b3 | ||
|  | 0a1067ec7d | ||
|  | 48756e8774 | ||
|  | 4915ad8fc7 | ||
|  | ba128cbae9 | ||
|  | 2a6e51493e | ||
|  | ef678f9f8a | ||
|  | e997ee7071 | ||
|  | 1273390af3 | ||
|  | 0f6eb4a244 | ||
|  | 2c0c62c89b | ||
|  | ec13c952d9 | ||
|  | 6fc71cf6f8 | ||
|  | bd3137a3dc | ||
|  | b26da4f97f | ||
|  | cde0adc28e | ||
|  | 2813f101b8 | ||
|  | 4d8fb8c176 | ||
|  | 9ae62d60f7 | ||
|  | 17c14e137e | ||
|  | bc239f6608 | ||
|  | 6493fad8a4 | ||
|  | d539f459ca | ||
|  | 2cca6a2f39 | ||
|  | 80e07bae0d | ||
|  | d4c4f6e1a5 | ||
|  | d24fe4e983 | ||
|  | aaa9042810 | ||
|  | a4bb1cefb9 | ||
|  | c3339b47bb | ||
|  | 0c1e5ea881 | ||
|  | 3fbd9006ad | ||
|  | be9761146c | ||
|  | 5aa9e7e0f6 | ||
|  | 65e87aea52 | ||
|  | 66fffc932c | ||
|  | 1e934e16af | ||
|  | 82fbb7e32d | ||
|  | 8bf9b9bef9 | ||
|  | 2d15e4f976 | ||
|  | 055ce56a57 | ||
|  | f1f2a61dc8 | ||
|  | 39c1b37726 | ||
|  | 5b1b25fa86 | ||
|  | 54ab0b1bf0 | ||
|  | 5507e9ffe9 | ||
|  | 30d5bfe59e | ||
|  | b716ec33d9 | ||
|  | e25209d9f9 | ||
|  | 1f4ac82495 | ||
|  | 0baf58f3fd | ||
|  | 9a769203e3 | ||
|  | 1435efaea7 | ||
|  | c411ac821e | ||
|  | da0489fc0c | ||
|  | c82de1e314 | ||
|  | 604a555e14 | ||
|  | 87e011ff96 | ||
|  | 9b2051827c | ||
|  | 47aefb0c82 | ||
|  | 70da1d6f5c | ||
|  | c0743ce9de | ||
|  | fe7b4044e0 | ||
|  | 701ce8ad47 | ||
|  | 591f907134 | ||
|  | 4f1db749c0 | ||
|  | ae72ed8c67 | ||
|  | ab7934fa98 | ||
|  | a4c77f0c9e | ||
|  | 0023c64d59 | ||
|  | b55b3bd63b | ||
|  | cbda20f67e | ||
|  | e598dbb206 | ||
|  | 094881fc5c | ||
|  | b13414cab8 | ||
|  | 3e65ac653b | ||
|  | 05621c9876 | ||
|  | 938619ce0c | ||
|  | 8e5267d44f | ||
|  | 3b55886c45 | ||
|  | ed3aceb427 | ||
|  | c3b2aaec69 | ||
|  | bb1d3431cc | ||
|  | fe83fe338a | ||
|  | 6504b5e818 | ||
|  | 8578900bfb | ||
|  | d730dd04bf | ||
|  | 1d90c5e1fe | ||
|  | 0f39b7733c | ||
|  | 038b6765e7 | ||
|  | 58569a648c | ||
|  | df0f048ecc | ||
|  | 288a008e72 | ||
|  | 439690b981 | ||
|  | b4e75f6844 | ||
|  | 3f6a5a7772 | ||
|  | a2d7cdcfb4 | ||
|  | d995ce38ea | ||
|  | 0b21a22027 | ||
|  | 111d75b351 | ||
|  | b54cac72ec | ||
|  | 69b2dd698c | ||
|  | 16bf19d115 | ||
|  | c57c9c2a8f | ||
|  | 9f750e5980 | ||
|  | 8d296e563d | ||
|  | 88f7570caf | ||
|  | 00a7c6b5be | ||
|  | 612b06346d | ||
|  | 9620566fbb | ||
|  | ed65c70a36 | ||
|  | cdd87436be | ||
|  | a143ebc8e0 | ||
|  | 93581ca4d9 | ||
|  | c5e9c9a683 | ||
|  | 1bdab10872 | ||
|  | eca339ad60 | ||
|  | af5eb3447f | ||
|  | b72cb52a71 | ||
|  | d0565a15d3 | ||
|  | 468bf1d6a0 | ||
|  | 1d2637c980 | ||
|  | a52c68d852 | ||
|  | e12f57d872 | ||
|  | 5def02ff7f | ||
|  | 7cab6eb680 | ||
|  | dcaa3526a4 | ||
|  | 2a989bc235 | ||
|  | d588fab981 | ||
|  | 9cb006b871 | ||
|  | 9056abaf4a | ||
|  | 3c27fd10b6 | ||
|  | 4048d79fc5 | ||
|  | 212e60c12d | ||
|  | 45142bc1bc | ||
|  | e791a663c8 | ||
|  | 81ae95eba7 | ||
|  | 7ca5d81123 | ||
|  | e234d2379b | ||
|  | d0838d53c2 | ||
|  | 880e2160a3 | ||
|  | c214d776df | ||
|  | 0a8220ca52 | ||
|  | b0c57272c2 | ||
|  | fbdebcae8a | ||
|  | 8e376deaaa | ||
|  | 2d353ffa35 | ||
|  | b70a9c36fa | ||
|  | c98967cb1e | ||
|  | 05f0d1855c | ||
|  | cee5043625 | ||
|  | 528358f3c3 | ||
|  | da90412cea | ||
|  | e794aab012 | ||
|  | 69daf14ffd | ||
|  | afa6c4270d | ||
|  | be22309e45 | ||
|  | 6f27e5ae07 | ||
|  | 7e79547973 | ||
|  | a5668b1b99 | ||
|  | f04dd6b6cd | ||
|  | 9f9c17ea8a | ||
|  | 48845ab60e | ||
|  | 59bc2318f8 | ||
|  | c622db918b | ||
|  | 4b381f33b5 | ||
|  | 7cfc7b9d74 | ||
|  | 7f52f9bcf2 | ||
|  | c4e7e42cdd | ||
|  | d181365620 | ||
|  | 97b474665a | ||
|  | 284b01c524 | ||
|  | e3857ca0f4 | ||
|  | 0aee42255f | ||
|  | fcb0d33e29 | ||
|  | 3a761f2082 | ||
|  | ed0ad7b988 | ||
|  | c392b5a661 | ||
|  | 766848eea3 | ||
|  | 7ede537d1b | ||
|  | ec38f90662 | ||
|  | d793b82d51 | ||
|  | aafeb1e875 | ||
|  | 480c10b239 | ||
|  | 82bb2544de | ||
|  | b714abd9a0 | ||
|  | c1be0180f9 | ||
|  | 2cfafe7bfe | ||
|  | da8edbfdc1 | ||
|  | 54bcbbc1de | ||
|  | 5463183e01 | ||
|  | 42b80b18f8 | ||
|  | 65fada7ef1 | ||
|  | 9e4997aa81 | ||
|  | e8c6ef945a | ||
|  | 14a22efae1 | ||
|  | f7f0bc8681 | ||
|  | 9cb8a135e6 | ||
|  | a02bce6517 | ||
|  | 9604c3a187 | ||
|  | 1ba4213910 | ||
|  | 6c63fac240 | ||
|  | b3dd531abb | ||
|  | 4ce42d4f70 | ||
|  | 4325fcdca2 | ||
|  | 8cf9d5f3ed | ||
|  | 8cb6015930 | ||
|  | f3cefde7cb | ||
|  | 92fcdde60d | ||
|  | 2dcf55ea89 | ||
|  | 8537f40f1d | ||
|  | 8417590893 | ||
|  | 4c43f5db15 | ||
|  | eb32ec89b4 | ||
|  | ad66c02735 | ||
|  | e8c0611db4 | ||
|  | 7fa54642e1 | ||
|  | 0e6d40f96c | ||
|  | cd1fe4e182 | ||
|  | c56b601ab8 | ||
|  | e78fa11fd4 | ||
|  | 1619dd29e9 | ||
|  | 793f3dd75c | ||
|  | ce05f8e958 | ||
|  | c8c4585946 | ||
|  | 990d1adfb2 | ||
|  | 404343de1d | ||
|  | 24bfaaca7e | ||
|  | 060ca43fc8 | ||
|  | b87e802c8d | ||
|  | 0f596d5620 | ||
|  | 3ebdb67bc0 | ||
|  | a5668ef729 | ||
|  | 24fccf6c44 | ||
|  | e12692252e | ||
|  | a6cbc9f0ef | ||
|  | be073daaf2 | ||
|  | 4c919a7489 | ||
|  | 7a7ed48063 | ||
|  | 01010fe795 | ||
|  | 32382dc461 | ||
|  | bf83341fb9 | ||
|  | c4d2a7b409 | ||
|  | 4ad832dcdd | ||
|  | b35d25eda4 | ||
|  | da51a5512f | ||
|  | 710d51bf32 | ||
|  | 446bb229bc | ||
|  | c605023bad | ||
|  | 28d866c001 | ||
|  | 95b587381b | ||
|  | fc14431147 | ||
|  | 5d8f2f5da1 | ||
|  | cf428dc1f7 | ||
|  | c579989ded | ||
|  | a954a62762 | ||
|  | 855860c00c | ||
|  | 58c48b1b21 | ||
|  | 512c392386 | ||
|  | c5ed70dbfc | ||
|  | f75ca0c399 | ||
|  | 75c2977daf | ||
|  | b3ef4b40c5 | ||
|  | bd5664cf8e | ||
|  | 272219eb61 | ||
|  | acbcb47739 | ||
|  | 672575e427 | ||
|  | be0b3b1d63 | ||
|  | c88d25b4ee | ||
|  | 9b83862a96 | ||
|  | 21c7f0da6e | ||
|  | dca5759569 | ||
|  | 0abf87bfa2 | ||
|  | 6b8b37843b | ||
|  | fa0b83f056 | ||
|  | a706fa3590 | ||
|  | 852aa3d260 | ||
|  | c4edcaad87 | ||
|  | 9d0cab01d5 | ||
|  | c3e507234d | ||
|  | 405ec82dd0 | ||
|  | 9c4c77fe84 | ||
|  | 831afdf9e7 | ||
|  | 1996efb183 | ||
|  | 19bb2a0238 | ||
|  | af6cf70558 | ||
|  | 9b57e6049e | ||
|  | f54c06fb94 | ||
|  | 2107e7c427 | ||
|  | eae277f165 | ||
|  | a9c4ad2895 | ||
|  | e88ba1ab1c | ||
|  | 677dcb87ef | ||
|  | 7bb217636e | ||
|  | abc16e98eb | ||
|  | 3b1405609e | ||
|  | b0d3518c1d | ||
|  | aed005618e | ||
|  | 559970c95d | ||
|  | f6f3aa11ea | ||
|  | 472c91f022 | ||
|  | 4a7f498683 | ||
|  | 2b87d6ec01 | ||
|  | 1af282a7a1 | ||
|  | 46b63f52fe | ||
|  | 01977005fb | ||
|  | b1a46b365b | ||
|  | 3a0a02d3ba | ||
|  | 1acbef5bfa | ||
|  | 66b2ba07bb | ||
|  | 4777538103 | ||
|  | 31d31fc3d3 | ||
|  | 21f606c6ba | ||
|  | 6508ec2d17 | ||
|  | c623ca9fe0 | ||
|  | 90deaf564f | ||
|  | e2d2cf67fa | ||
|  | 8ee73a11bb | ||
|  | b5b01be373 | ||
|  | 730f2eb0b8 | ||
|  | 4576754de2 | ||
|  | 0fb9e0bc89 | ||
|  | 000c34ef73 | ||
|  | 0ec80a7791 | ||
|  | e9650285bd | ||
|  | d0cc0a4034 | ||
|  | f9c4a3a9c0 | ||
|  | 15fd2021bb | ||
|  | 75cf1d610e | ||
|  | 8f5ee9466a | ||
|  | 467720f1c4 | ||
|  | 026e624e23 | ||
|  | 460a9fc5f7 | ||
|  | 3695e64ce9 | ||
|  | b3bb829e4d | ||
|  | 1b9f8c23d2 | ||
|  | 5ee5fd7332 | ||
|  | 18d94b9a26 | ||
|  | 3ae9686b2b | ||
|  | bbe4ef8497 | ||
|  | a2fd354dc9 | ||
|  | a7202078ce | ||
|  | d8940a9cea | ||
|  | 96611333bf | ||
|  | 5074123f57 | ||
|  | 5bfbd3eaa0 | ||
|  | 7fdeed14f5 | ||
|  | 04a9ca92b5 | ||
|  | 016a1e9adb | ||
|  | 1d12e57606 | ||
|  | cb753f5371 | ||
|  | 7a9f9b2705 | ||
|  | 6d4b4dd9fc | ||
|  | 3d9c92fb63 | ||
|  | d4168f6b5d | ||
|  | 1751cabb9d | ||
|  | b0e3b6414a | ||
|  | 51967ed9f5 | ||
|  | fc06f13c30 | ||
|  | b094707324 | ||
|  | e4a0a517d5 | ||
|  | 222a5f3779 | ||
|  | 72f7106086 | ||
|  | 33387b60cc | ||
|  | 83cc18f648 | ||
|  | 1735ca57d5 | ||
|  | a49459b253 | ||
|  | 5a4fbbf48a | ||
|  | f7465679c0 | ||
|  | 2a5299ebcb | ||
|  | b3bddb2c99 | ||
|  | 997aec8cc2 | ||
|  | c67320f185 | ||
|  | 8224a6ac35 | ||
|  | abcee8aa56 | ||
|  | 23c6b44921 | ||
|  | 1034171e91 | ||
|  | bf49f79e6e | ||
|  | f7f24dbdfe | ||
|  | 31ec848aec | ||
|  | 9eb08420b6 | ||
|  | eb6d8d4f83 | ||
|  | e10f620cf9 | ||
|  | f750436b64 | ||
|  | a4a8504558 | ||
|  | 385e5aabaa | ||
|  | d831315e20 | ||
|  | e0906f3462 | ||
|  | 6595b6a44f | ||
|  | 0183d7a080 | ||
|  | 89e803ee42 | ||
|  | c0ce52abe3 | ||
|  | a1c7f20990 | ||
|  | 0ef6f89d44 | ||
|  | 54b04962a8 | ||
|  | 60f8de282d | ||
|  | b4350278a0 | ||
|  | 332e429a41 | ||
|  | 7be25c21ed | ||
|  | 1dfa5d383c | ||
|  | c2d2c278e0 | ||
|  | 75a57829c2 | ||
|  | c3c5307624 | ||
|  | 879d585f2e | ||
|  | 9969fede35 | ||
|  | c15b31b374 | ||
|  | a3a6c33b32 | ||
|  | 236bf6e0fc | ||
|  | 974de179e0 | ||
|  | 60e03777f3 | ||
|  | 05f6b2510f | ||
|  | f3274977f5 | ||
|  | cf7fb1a3b9 | ||
|  | 12457a87d4 | ||
|  | 9a8de7db80 | ||
|  | 68e02a528e | ||
|  | 277055f44e | ||
|  | c3fc745c7e | ||
|  | 8901ed219d | ||
|  | d7c70dc021 | ||
|  | 53c789bff9 | ||
|  | eb63745664 | ||
|  | 91d72e48ad | ||
|  | 1dcda63192 | ||
|  | 3ea6544f77 | ||
|  | d12a587f11 | ||
|  | 2b147bb98b | ||
|  | 6529eaaf9a | ||
|  | a68aa148b8 | ||
|  | 98942d6f9c | ||
|  | 690efc0aff | ||
|  | 627cfa1dff | ||
|  | 99aca932db | ||
|  | dd43f35bbe | ||
|  | 093988e136 | ||
|  | 087a472765 | ||
|  | ce13d7f98b | ||
|  | b1fc766908 | ||
|  | 22384342db | ||
|  | badce5146a | ||
|  | 0bf3ff9c17 | ||
|  | 860840c367 | ||
|  | 221b6325f6 | ||
|  | 06389e35f9 | ||
|  | a7756cec13 | ||
|  | 9ee11654a6 | ||
|  | a273266e5e | ||
|  | e2158716d6 | ||
|  | c132206543 | ||
|  | f49312ed13 | ||
|  | a9f69f07e6 | ||
|  | d7429a4812 | ||
|  | be76d5ce9a | ||
|  | f2bc10c5c0 | ||
|  | 43eb760bce | ||
|  | a7684e3e9f | ||
|  | 8949aa3bec | ||
|  | e40ddd4b69 | ||
|  | 17637fb1f6 | ||
|  | f71defe958 | ||
|  | b8d6a6da1f | ||
|  | fbc886b21c | ||
|  | 8879fb55de | ||
|  | ed316e8bf3 | ||
|  | 45529f7de9 | ||
|  | dbdff38d2e | ||
|  | 21c3179e03 | ||
|  | c05d93aed8 | ||
|  | 6225abb010 | ||
|  | c41fff8f5c | ||
|  | 8589d9c482 | ||
|  | 7f264953af | ||
|  | cfff2ad72b | ||
|  | c0258b352e | ||
|  | 3e1b051ec3 | ||
|  | b34bb87d7a | ||
|  | 83b8cc6729 | ||
|  | 878c0be727 | ||
|  | e7f06f5c0c | ||
|  | a1fc38c5fe | ||
|  | ff0ab24000 | ||
|  | 56a10d192d | ||
|  | 1a8413d8f0 | ||
|  | 934b156ebb | ||
|  | f9639d9705 | ||
|  | 4c345c4f33 | ||
|  | 490151267a | ||
|  | 3d19a08cc7 | ||
|  | 385c4c32f9 | ||
|  | 3a5052c871 | ||
|  | f84f590f1d | ||
|  | 5b9eba7819 | ||
|  | cd18794fca | ||
|  | ae3d552ad7 | ||
|  | be5f71f4a7 | ||
|  | 745a5f0376 | ||
|  | 99114b2a61 | ||
|  | df9bd2b0f9 | ||
|  | e194b559ac | ||
|  | af5344dccd | ||
|  | 2d7b8121d7 | ||
|  | 0297450702 | ||
|  | 6b17f6aa28 | ||
|  | 8a7abfe42d | ||
|  | dd5041395c | ||
|  | 36d6a5bc15 | ||
|  | 2619f92d09 | ||
|  | 53720ae8ae | ||
|  | bcff953fbb | ||
|  | bcc0cc599d | ||
|  | a1e3fed312 | ||
|  | 399dca2ef9 | ||
|  | 2e44e1626d | ||
|  | 39aa2dfe01 | ||
|  | 099929c677 | ||
|  | af5d132410 | ||
|  | 79acbc3a98 | ||
|  | e75e4e2284 | ||
|  | 9aa0af4f9c | ||
|  | 50e272efba | ||
|  | 209e049893 | ||
|  | bbb3accf0c | ||
|  | 179989aa42 | ||
|  | 2881d19d43 | ||
|  | 7cfc3458ec | ||
|  | 659e1da79d | ||
|  | a7ae79493d | ||
|  | 8b484ee707 | ||
|  | d617d4aa09 | ||
|  | 99c04648b4 | ||
|  | f945d50c0d | ||
|  | e9fabd59ed | ||
|  | ad13de3588 | ||
|  | aad8141e27 | ||
|  | 26a76f80d6 | ||
|  | 53ead2087f | ||
|  | faee811d67 | ||
|  | b9c739df1f | ||
|  | 6e124842e8 | ||
|  | 7a5928ea24 | ||
|  | 7fdf7de11c | ||
|  | b75eedb84e | ||
|  | 3b92ae49a9 | ||
|  | 775d1091db | ||
|  | 1f77b491fc | ||
|  | eff2fd7cc0 | ||
|  | 58d2a0d874 | ||
|  | ea9def997a | ||
|  | a222c58047 | ||
|  | 39a838c2ab | ||
|  | cfc0bcd5ad | ||
|  | 3418c9b50f | ||
|  | e686611890 | ||
|  | ebb5dee1fc | ||
|  | d9edaffd9c | ||
|  | cbe7b1a5b9 | ||
|  | e758fd4093 | ||
|  | 14a99a3b25 | ||
|  | 1ba67506a0 | ||
|  | 2a6ca5d5ac | ||
|  | aa12e6495a | ||
|  | 9269848f66 | ||
|  | a71e61cd30 | ||
|  | 8be4604c97 | ||
|  | a7bba903f5 | ||
|  | 1d8af5835d | ||
|  | 189c01fc74 | ||
|  | e3a5bbf661 | ||
|  | ee23c5f72c | ||
|  | a2083be76b | ||
|  | a1c4be83d6 | ||
|  | d2fde2bfc8 | ||
|  | e8956b0b55 | ||
|  | 2af4009a93 | ||
|  | a5f7c946cc | ||
|  | 298542b531 | ||
|  | fca6707a29 | ||
|  | 10d3a284e9 | ||
|  | 1a244726aa | ||
|  | 044935a164 | ||
|  | 99e5edf2c5 | ||
|  | b26270bd13 | ||
|  | 65a8cb9ddb | ||
|  | ba4b976e80 | ||
|  | 297ae1dbaf | ||
|  | 214614f740 | ||
|  | 0e14d3d6e8 | ||
|  | 67011c0c32 | ||
|  | 16bbb42b8d | ||
|  | b85ac91e6c | ||
|  | 66759a33fa | ||
|  | bf5e83861c | ||
|  | d56a6fb06f | ||
|  | 5e7aa8e16d | ||
|  | bace0ad339 | ||
|  | 6014eaf8eb | ||
|  | 3e96e8b3f5 | ||
|  | 95d1b8a6d0 | ||
|  | 0ecb66c99e | ||
|  | af52b91799 | ||
|  | 3c50d6c30a | ||
|  | 2722c72c43 | ||
|  | d5ab3101c6 | ||
|  | 32df76bdff | ||
|  | 68a06c3d1d | ||
|  | 1b42dc779b | ||
|  | cedffd40f2 | ||
|  | cdc8db4837 | ||
|  | a0ee23d84e | ||
|  | 49d2d8c9d0 | ||
|  | 63620aa811 | ||
|  | d5b11a1dba | ||
|  | 4a63af0490 | ||
|  | a68019293f | ||
|  | 6b1c91f0dd | ||
|  | ea93785581 | ||
|  | 32819c4fd5 | ||
|  | fc5a438cdc | ||
|  | 57fe94f945 | ||
|  | aa3a3bdf16 | ||
|  | db89da3daa | ||
|  | d6ba5796ce | ||
|  | 3968743b28 | ||
|  | 20c6226b84 | ||
|  | 463ce394fe | ||
|  | 1faefebe42 | ||
|  | e2c9339ec4 | ||
|  | 4b1c7da171 | ||
|  | bdfd6e5e9f | ||
|  | 4c8508b0a9 | ||
|  | 06b3f92963 | ||
|  | d43a57af36 | ||
|  | aeefe28710 | ||
|  | e9de961a23 | ||
|  | dcec778e02 | ||
|  | b212641069 | ||
|  | 90aa50bb11 | ||
|  | a6879e853b | ||
|  | 37fab7ac63 | ||
|  | 8b01ae08c5 | ||
|  | b1cdf42790 | ||
|  | 974968d238 | ||
|  | bf467cbba5 | ||
|  | 536aa2e96e | ||
|  | 3d84344b75 | ||
|  | 1054ba3b1e | ||
|  | aa8ddb9a92 | ||
|  | fcfe57e5e2 | ||
|  | c4fd4e0317 | ||
|  | 3c76933824 | ||
|  | fa83819bee | ||
|  | b65ae88879 | ||
|  | 96db21f9bf | ||
|  | 6d356ff770 | ||
|  | 21790b32bf | ||
|  | 159f3d0aa2 | ||
|  | 012a7b0678 | ||
|  | 6595c85671 | ||
|  | cf5c0464fe | ||
|  | e31450f731 | ||
|  | 3653984a95 | ||
|  | 0c0b856c37 | ||
|  | 69f1b153ea | ||
|  | 43ba4bd00e | ||
|  | bf5edcaac6 | ||
|  | ac51709211 | ||
|  | e1a578e819 | ||
|  | 591c9e53b0 | ||
|  | 87d543eb3a | ||
|  | 40c1521591 | ||
|  | b31c2a6264 | ||
|  | 66a42f13f1 | ||
|  | 8de6ebbbd1 | ||
|  | 649de694ed | ||
|  | 8a52fde8fc | ||
|  | fb8bd657de | ||
|  | b04a0a6b61 | ||
|  | f29c911a0f | ||
|  | bd908123c2 | ||
|  | cbdb0b67ab | ||
|  | aa3848f420 | ||
|  | 6cf0748172 | ||
|  | 11122d3f81 | ||
|  | 2dea9398f2 | ||
|  | de93b3294f | ||
|  | 7accb84eb9 | ||
|  | ea90ed04d6 | ||
|  | 838eed2630 | ||
|  | 376b65c749 | ||
|  | 3b4432cb00 | ||
|  | 7bc71029de | ||
|  | d736dd92be | ||
|  | 6eba8d681c | ||
|  | 5fe654c19d | ||
|  | dd366f35a8 | ||
|  | 2ababa521d | ||
|  | bda8f26511 | ||
|  | 7f1a3df25b | ||
|  | ef2ff50089 | ||
|  | 0b3964c827 | ||
|  | ccf5bb9342 | ||
|  | 4303882c6a | ||
|  | c34028d549 | ||
|  | 8e76cdcb57 | ||
|  | 5eb66106b9 | ||
|  | 0abebc1e32 | ||
|  | 552e82f44d | ||
|  | ada40e36db | ||
|  | 480f734a06 | ||
|  | 20bee9c334 | ||
|  | de8267f41e | ||
|  | 7bfaf07980 | ||
|  | a42fa8e9f9 | ||
|  | 5facad683a | ||
|  | fd1913a72e | ||
|  | 54c98b4250 | ||
|  | 5d99baac21 | ||
|  | cb95bdf6d7 | ||
|  | 25e803abfc | ||
|  | 7c6073e4ef | ||
|  | 09bcbe8dfc | ||
|  | e0a9c7d0bb | ||
|  | ac27d05fd5 | ||
|  | d6ab56252f | ||
|  | dab178ed50 | ||
|  | 6ed50b6a75 | ||
|  | ee559ec650 | ||
|  | 4310238418 | ||
|  | 10c47a6c38 | ||
|  | 4b8043086e | ||
|  | fd952b88bf | ||
|  | e262d463c5 | ||
|  | b02bce2510 | ||
|  | 58094531c0 | ||
|  | d937e3ca7c | ||
|  | 9c58413209 | ||
|  | db65ff12d7 | ||
|  | f93b819ea6 | ||
|  | e6fea297e5 | ||
|  | 5c2a0e5634 | ||
|  | 71aa21a82d | ||
|  | 23821360c7 | ||
|  | 79f5b938f5 | ||
|  | f8769fcc2a | ||
|  | a3ed24c766 | ||
|  | 82727b825c | ||
|  | 16e98496af | ||
|  | 62896ce1a3 | ||
|  | 7ea5b1ecbf | ||
|  | eecc95f8fb | ||
|  | 85808d85c4 | ||
|  | 12cc670642 | ||
|  | 13fcb55df6 | ||
|  | a4dfd15888 | ||
|  | 7fbd326298 | ||
|  | 331d147d50 | ||
|  | acdcdc55bc | ||
|  | fdb0c0acb3 | ||
|  | 564bf47fb2 | ||
|  | eca35b2371 | ||
|  | f97d2e2644 | ||
|  | fdd6659139 | ||
|  | 0d698fb659 | ||
|  | c68b39dda8 | ||
|  | cb67286bc3 | ||
|  | a49962b8de | ||
|  | 256d5ae14f | ||
|  | 799ee8bcfa | ||
|  | fe8a317ef9 | ||
|  | b6d6ee45e0 | ||
|  | 0151466a28 | ||
|  | 3588875e28 | ||
|  | e8031aec39 | ||
|  | 1cabe107e6 | ||
|  | ba0c59744b | ||
|  | 50c702c00a | ||
|  | ff8c2fe227 | ||
|  | f9c78a5263 | ||
|  | d46784a4d5 | ||
|  | ff43d082f2 | ||
|  | 83801736d7 | ||
|  | e16986cf71 | ||
|  | 7ad12d954c | ||
|  | fb74fadec2 | ||
|  | c00bdf910e | ||
|  | 50cc1c56e5 | ||
|  | 0006099758 | ||
|  | 162dcec331 | ||
|  | 3e8bd022e2 | ||
|  | 01e5936671 | ||
|  | d85e1c70d6 | ||
|  | f329770194 | ||
|  | 08194925cb | ||
|  | 2b7aa3e810 | ||
|  | 549417f106 | ||
|  | f0f370cc7d | ||
|  | b842241f8c | ||
|  | 93aa4b7440 | ||
|  | 9e2eef7818 | ||
|  | b29b65851e | ||
|  | a6829bff4f | ||
|  | 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 | ||
|  | e51f6597ed | ||
|  | e80a65a3cd | ||
|  | 7c3675c9e1 | 
| @@ -1,10 +1,10 @@ | ||||
| { | ||||
| 	"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"], | ||||
| 	"plugins": ["prettier", "jsdoc"], | ||||
| 	"plugins": ["prettier", "import", "jsdoc", "jest"], | ||||
| 	"env": { | ||||
| 		"browser": true, | ||||
| 		"es6": true, | ||||
| 		"mocha": true, | ||||
| 		"es2022": true, | ||||
| 		"jest/globals": true, | ||||
| 		"node": true | ||||
| 	}, | ||||
| 	"globals": { | ||||
| @@ -16,15 +16,19 @@ | ||||
| 	}, | ||||
| 	"parserOptions": { | ||||
| 		"sourceType": "module", | ||||
| 		"ecmaVersion": 2017, | ||||
| 		"ecmaVersion": 2022, | ||||
| 		"ecmaFeatures": { | ||||
| 			"globalReturn": true | ||||
| 		} | ||||
| 	}, | ||||
| 	"rules": { | ||||
| 		"prettier/prettier": "error", | ||||
| 		"eqeqeq": "error", | ||||
| 		"import/order": "error", | ||||
| 		"no-param-reassign": "error", | ||||
| 		"no-prototype-builtins": "off", | ||||
| 		"no-unused-vars": "off" | ||||
| 		"no-throw-literal": "error", | ||||
| 		"no-unused-vars": "off", | ||||
| 		"no-useless-return": "error", | ||||
| 		"prefer-template": "error" | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										56
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| # .gitattributes snippet to force users to use same line endings for project. | ||||
| #  | ||||
| # Handle line endings automatically for files detected as text | ||||
| # and leave all files detected as binary untouched. | ||||
| * text=auto | ||||
|  | ||||
| # | ||||
| # The above will handle all files NOT found below | ||||
| # https://help.github.com/articles/dealing-with-line-endings/ | ||||
| # https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes | ||||
|  | ||||
|  | ||||
|  | ||||
| # These files are text and should be normalized (Convert crlf => lf) | ||||
| *.php text | ||||
| *.css text | ||||
| *.scss text | ||||
| *.js text | ||||
| *.json text | ||||
| *.htm text | ||||
| *.html text | ||||
| *.xml text | ||||
| *.txt text | ||||
| *.ini text | ||||
| *.inc text | ||||
| *.pl text | ||||
| *.rb text | ||||
| *.py text | ||||
| *.scm text | ||||
| *.sql text | ||||
| .htaccess text | ||||
| *.sh text | ||||
| Dockerfile* text | ||||
| *.yml text | ||||
| *.yaml text | ||||
| *.md text | ||||
| *.markdown text | ||||
|  | ||||
| # These files are binary and should be left untouched | ||||
| # (binary is a macro for -text -diff) | ||||
| *.png binary | ||||
| *.jpg binary | ||||
| *.jpeg binary | ||||
| *.gif binary | ||||
| *.ico binary | ||||
| *.mov binary | ||||
| *.mp4 binary | ||||
| *.mp3 binary | ||||
| *.flv binary | ||||
| *.fla binary | ||||
| *.swf binary | ||||
| *.gz binary | ||||
| *.zip binary | ||||
| *.7z binary | ||||
| *.ttf binary | ||||
| *.pyc binary | ||||
							
								
								
									
										28
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							| @@ -4,36 +4,48 @@ Thanks for contributing to MagicMirror²! | ||||
|  | ||||
| We hold our code to standard, and these standards are documented below. | ||||
|  | ||||
| If you wish to run our linters, use `npm run lint` without any arguments. | ||||
| ## Linters | ||||
|  | ||||
| We use prettier for automatic linting of all our files: `npm run lint:prettier`. | ||||
|  | ||||
| ### JavaScript: Run ESLint | ||||
|  | ||||
| We use [ESLint](https://eslint.org) on our JavaScript files. | ||||
|  | ||||
| Our ESLint configuration is in our .eslintrc.json and .eslintignore files. | ||||
| Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files. | ||||
|  | ||||
| To run ESLint, use `npm run lint:js`. | ||||
|  | ||||
| ### CSS: Run StyleLint | ||||
|  | ||||
| We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file. | ||||
| We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our `.stylelintrc` file. | ||||
|  | ||||
| To run StyleLint, use `npm run lint:style`. | ||||
| To run StyleLint, use `npm run lint:css`. | ||||
|  | ||||
| ### Submitting Issues | ||||
| ## Testing | ||||
|  | ||||
| We use [Jest](https://jestjs.io) for JavaScript testing. | ||||
|  | ||||
| To run all tests, use `npm run test`. | ||||
|  | ||||
| The specific test commands are defined in `package.json`. | ||||
| So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`. | ||||
|  | ||||
| ## Submitting Issues | ||||
|  | ||||
| Please only submit reproducible issues. | ||||
|  | ||||
| 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 18 or later (recommended is 20). | ||||
|  | ||||
| **MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2). | ||||
| **MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file. | ||||
|  | ||||
| **Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/FUNDING.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| github: MichMich | ||||
| custom: ["https://magicmirror.builders/#donate"] | ||||
| @@ -1,3 +1,7 @@ | ||||
| Hello and thank you for opening an issue. | ||||
| 
 | ||||
| **Please make sure that you have read the following lines before submitting your Issue:** | ||||
| 
 | ||||
| ## I'm not sure if this is a bug | ||||
| 
 | ||||
| If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt) | ||||
| @@ -6,15 +10,21 @@ If you're not sure if it's a real bug or if it's just you, please open a topic o | ||||
| 
 | ||||
| Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting) | ||||
| 
 | ||||
| ## I found a bug in the MagicMirror installer | ||||
| 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. | ||||
| 
 | ||||
| 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: | ||||
| ## 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 | ||||
| ## I found a bug in the MagicMirror² Docker image | ||||
| 
 | ||||
| If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image: | ||||
| [https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror) | ||||
| If you are facing an issue or found a bug while running MagicMirror² inside a Docker container please create an issue in the corresponding repository: | ||||
| [https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror) | ||||
| 
 | ||||
| ## I'm having troubles installing or configuring foreign modules | ||||
| 
 | ||||
| Please open an issue in the module repository or ask for help in the [forum](https://forum.magicmirror.builders/) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| @@ -23,11 +33,11 @@ If you are facing an issue or found a bug while running MagicMirror inside a Doc | ||||
| 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 18 or later (recommended is 20). | ||||
| 
 | ||||
| **MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file. | ||||
| **MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file. | ||||
| 
 | ||||
| **Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. | ||||
| 
 | ||||
							
								
								
									
										24
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,23 @@ | ||||
| > 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! | ||||
|   | ||||
							
								
								
									
										10
									
								
								.github/codecov.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.github/codecov.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| coverage: | ||||
|   status: | ||||
|     project: | ||||
|       default: | ||||
|         # advanced settings | ||||
|         informational: true | ||||
|     patch: | ||||
|       default: | ||||
|         threshold: 0% | ||||
|         target: 0 | ||||
							
								
								
									
										25
									
								
								.github/dependabot.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.github/dependabot.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
|     target-branch: "develop" | ||||
|  | ||||
|   - package-ecosystem: "npm" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "monthly" | ||||
|     target-branch: "develop" | ||||
|  | ||||
|   - package-ecosystem: "npm" | ||||
|     directory: "/vendor" | ||||
|     schedule: | ||||
|       interval: "monthly" | ||||
|     target-branch: "develop" | ||||
|  | ||||
|   - package-ecosystem: "npm" | ||||
|     directory: "/fonts" | ||||
|     schedule: | ||||
|       interval: "monthly" | ||||
|     target-branch: "develop" | ||||
							
								
								
									
										
											BIN
										
									
								
								.github/header.psd
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.github/header.psd
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										0
									
								
								.github/stale.yml → .github/stale.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										0
									
								
								.github/stale.yml → .github/stale.yaml
									
									
									
									
										vendored
									
									
								
							
							
								
								
									
										41
									
								
								.github/workflows/automated-tests.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								.github/workflows/automated-tests.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||||
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||||
|  | ||||
| name: "Run Automated Tests" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master, develop] | ||||
|   pull_request: | ||||
|     branches: [master, develop] | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 30 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [18.x, 20.x] | ||||
|     steps: | ||||
|       - name: "Checkout code" | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: "Use Node.js ${{ matrix.node-version }}" | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: ${{ matrix.node-version }} | ||||
|           cache: "npm" | ||||
|       - name: "Install dependencies" | ||||
|         run: | | ||||
|           npm run install-mm:dev | ||||
|       - name: "Run tests" | ||||
|         run: | | ||||
|           Xvfb :99 -screen 0 1024x768x16 & | ||||
|           export DISPLAY=:99 | ||||
|           touch css/custom.css | ||||
|           npm run test:prettier | ||||
|           npm run test:js | ||||
|           npm run test:css | ||||
|           npm run test | ||||
							
								
								
									
										35
									
								
								.github/workflows/codecov-test-suites.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/codecov-test-suites.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| # This workflow runs the automated test and uploads the coverage results to codecov.io | ||||
| # For more information see: https://github.com/codecov/codecov-action | ||||
|  | ||||
| name: "Run Codecov Tests" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master, develop] | ||||
|   pull_request: | ||||
|     branches: [master, develop] | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   run-and-upload-coverage-report: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 30 | ||||
|     steps: | ||||
|       - name: "Checkout code" | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: "Install dependencies" | ||||
|         run: | | ||||
|           npm ci | ||||
|       - name: "Run coverage" | ||||
|         run: | | ||||
|           Xvfb :99 -screen 0 1024x768x16 & | ||||
|           export DISPLAY=:99 | ||||
|           touch css/custom.css | ||||
|           npm run test:coverage | ||||
|       - name: "Upload coverage results to codecov" | ||||
|         uses: codecov/codecov-action@v3 | ||||
|         with: | ||||
|           files: ./coverage/lcov.info | ||||
|           fail_ci_if_error: true | ||||
							
								
								
									
										18
									
								
								.github/workflows/depsreview.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/depsreview.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # This workflow scans your pull requests for dependency changes, and will raise an error if any vulnerabilities or invalid licenses are being introduced. | ||||
| # For more information see: https://github.com/actions/dependency-review-action | ||||
|  | ||||
| name: "Review Dependencies" | ||||
|  | ||||
| on: [pull_request] | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   dependency-review: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: "Checkout code" | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: "Dependency Review" | ||||
|         uses: actions/dependency-review-action@v3 | ||||
							
								
								
									
										15
									
								
								.github/workflows/enforce-changelog.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/enforce-changelog.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,15 +0,0 @@ | ||||
| name: "Enforce Changelog" | ||||
| on: | ||||
|   pull_request: | ||||
|       types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] | ||||
|  | ||||
| jobs: | ||||
|   # Enforces the update of a changelog file on every pull request  | ||||
|   check: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - uses: dangoslen/changelog-enforcer@v1.6.1 | ||||
|       with: | ||||
|         changeLogPath: 'CHANGELOG.md' | ||||
|         skipLabels: 'Skip Changelog' | ||||
							
								
								
									
										28
									
								
								.github/workflows/enforce-pullrequest-rules.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/enforce-pullrequest-rules.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # This workflow enforces on every pull request: | ||||
| # - the update of our CHANGELOG.md file, see: https://github.com/dangoslen/changelog-enforcer | ||||
| # - that the PR is not based against master, taken from https://github.com/oppia/oppia-android/pull/2832/files | ||||
|  | ||||
| name: "Enforce Pull-Request Rules" | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
|     types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] | ||||
|  | ||||
| jobs: | ||||
|   check: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 10 | ||||
|     steps: | ||||
|       - name: "Enforce changelog" | ||||
|         uses: dangoslen/changelog-enforcer@v3 | ||||
|         with: | ||||
|           changeLogPath: "CHANGELOG.md" | ||||
|           skipLabels: "Skip Changelog" | ||||
|       - name: "Enforce develop branch" | ||||
|         if: ${{ github.base_ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }} | ||||
|         run: | | ||||
|           echo "This PR is based against the master branch and not a release or hotfix." | ||||
|           echo "Please don't do this. Switch the branch to 'develop'." | ||||
|           exit 1 | ||||
|         env: | ||||
|           BASE_BRANCH: ${{ github.base_ref }} | ||||
							
								
								
									
										35
									
								
								.github/workflows/node-ci.js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/node-ci.js.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,35 +0,0 @@ | ||||
| # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||||
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||||
|  | ||||
| name: Automated Tests | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master, develop ] | ||||
|   pull_request: | ||||
|     branches: [ master, develop ] | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|  | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [10.x, 12.x, 14.x] | ||||
|  | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Use Node.js ${{ matrix.node-version }} | ||||
|       uses: actions/setup-node@v1 | ||||
|       with: | ||||
|         node-version: ${{ matrix.node-version }} | ||||
|     - run: | | ||||
|         Xvfb :99 -screen 0 1024x768x16 & | ||||
|         export DISPLAY=:99 | ||||
|         npm install | ||||
|         npm run test:prettier | ||||
|         npm run test:js | ||||
|         npm run test:css | ||||
|         npm run test:e2e | ||||
|         npm run test:unit | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,6 @@ vendor/node_modules/**/* | ||||
| jspm_modules | ||||
| .npm | ||||
| .node_repl_history | ||||
| .nyc_output/ | ||||
|  | ||||
| # Visual Studio Code ignoramuses. | ||||
| .vscode/ | ||||
|   | ||||
							
								
								
									
										7
									
								
								.husky/pre-commit
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								.husky/pre-commit
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| [ -f "$(dirname "$0")/_/husky.sh" ] && . "$(dirname "$0")/_/husky.sh" | ||||
|  | ||||
| if command -v npm &> /dev/null; then | ||||
|   npm run lint:staged | ||||
| fi | ||||
| @@ -1,5 +1,3 @@ | ||||
| /config | ||||
| /coverage | ||||
| package-lock.json | ||||
| /config/**/* | ||||
| /vendor/**/* | ||||
| !/vendor/vendor.js | ||||
| .github/**/* | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| { | ||||
| 	"extends": ["stylelint-prettier/recommended"], | ||||
| 	"extends": ["stylelint-config-standard"], | ||||
| 	"plugins": ["stylelint-prettier"], | ||||
| 	"rules": { | ||||
| 		"prettier/prettier": true | ||||
|   | ||||
							
								
								
									
										576
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										576
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -3,7 +3,510 @@ | ||||
| All notable changes to this project will be documented in this file. | ||||
| This project adheres to [Semantic Versioning](https://semver.org/). | ||||
|  | ||||
| ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror² | ||||
| ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror². | ||||
|  | ||||
| ## [2.25.0] - 2023-10-01 | ||||
|  | ||||
| Thanks to: @bugsounet, @dgoth, @dependabot, @kenzal, @Knapoc, @KristjanESPERANTO, @martingron, @NolanKingdon, @Paranoid93, @TeddyStarinvest and @Ybbet. | ||||
|  | ||||
| Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! | ||||
|  | ||||
| > ⚠️ This release needs nodejs version >= `v18`, older releases have reached end of life and will not work! | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added UV Index support to OpenWeatherMap | ||||
| - Added 'hideDuplicates' flag to the calendar module | ||||
| - Added `allowOverrideNotification` to weather module to enable sending current weather objects with the `CURRENT_WEATHER_OVERRIDE` notification to supplement/replace the current weather displayed | ||||
| - Added optional AnimateCSS animate for `hide()`, `show()`, `updateDom()` | ||||
| - Added AnimateIn and animateOut in module config definition | ||||
| - Apply AnimateIn rules on the first start | ||||
| - Added automatic client page reload when server was restarted by setting `reloadAfterServerRestart: true` in `config.js`, per default `false` (#3105) | ||||
| - Added eventClass option for customEvents on the default calendar | ||||
| - Added AnimateCSS integration in tests suite (#3206) | ||||
| - Added npm dependabot [Reserved to developer] (#3210) | ||||
| - Added improved logging for calendar (#3110) | ||||
|  | ||||
| ### Removed | ||||
|  | ||||
| - **Breaking Change**: Removed `digest` authentication method from calendar module (which was already broken since release `2.15.0`) | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Update roboto fonts to version v5 | ||||
| - Update issue template | ||||
| - Update dev/dependencies incl. electron to v26 | ||||
| - Replace pretty-quick by lint-staged (<https://github.com/azz/pretty-quick/issues/164>) | ||||
| - Update engine node >=18. v16 reached it's end of life. (#3170) | ||||
| - Update typescript definition for modules | ||||
| - Cleaned up nunjuck templates | ||||
| - Replace `node-fetch` with internal fetch (#2649) and remove `digest-fetch` | ||||
| - Update the French translation according to the English file. | ||||
| - Update dependabot incl. vendor/fonts (monthly check) | ||||
| - Renew `package-lock.json` for release | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fix engine check on npm install (#3135) | ||||
| - Fix undefined formatTime method in clock module (#3143) | ||||
| - Fix clientonly startup fails after async added (#3151) | ||||
| - Fix electron width/heigth when using xrandr under bullseye | ||||
| - Fix time issue with certain recurring events in calendar module | ||||
| - Fix ipWhiteList test (#3179) | ||||
| - Fix newsfeed: Convert HTML entities, codes and tag in description (#3191) | ||||
| - Respect width/height (no fullscreen) if set in electronOptions (together with `fullscreen: false`) in `config.js` (#3174) | ||||
| - Fix: AnimateCSS merge hide() and show() animated css class when we do multiple call | ||||
| - Fix `Uncaught SyntaxError: Identifier 'getCorsUrl' has already been declared (at utils.js:1:1)` when using `clock` and `weather` module (#3204) | ||||
| - Fix overriding `config.js` when running tests (#3201) | ||||
| - Fix issue in weathergov provider with probability of precipitation not showing up on hourly or daily forecast | ||||
|  | ||||
| ## [2.24.0] - 2023-07-01 | ||||
|  | ||||
| Thanks to: @angeldeejay, @bugsounet, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @eddiehung, @grenagit, @Hirschberger, @ismarslomic, @JakeBinney, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @oscarb, @OWL4C, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt | ||||
|  | ||||
| Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added UV Index to hourly and current Weather, with support for Openmeteo | ||||
| - Added tests for serveronly | ||||
| - Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests) | ||||
| - Added no-param-reassign eslint rule and fix warnings | ||||
| - updatenotification: Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules | ||||
| - updatenotification: allow force scanning with `SCAN_UPDATES` notification from other modules | ||||
| - Added per-calendar fetchInterval | ||||
|  | ||||
| ### Removed | ||||
|  | ||||
| - Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896, second attempt ...) | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Added support for precipitation probability with openmeteo weather-provider | ||||
| - Update electron to v25.2 and other dependencies | ||||
| - Use node v20 in github workflow (replacing v14) | ||||
| - Refactor formatTime into common util function for default modules | ||||
| - Refactor some calendar methods into own class and added tests for them | ||||
| - Split install and run commands in github actions | ||||
| - Changed `fetchInterval` of calendar in `config.js.sample` to 7 days so we not to request example calendar too frequently | ||||
| - Changed default calendar fetchInterval to one hour | ||||
| - Changed calendar url in sample config | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fix envcanada hourly forecast time (#3080) | ||||
| - Fix electron not running under windows after async changes (#3083) | ||||
| - Fix style issues after eslint-plugin-jsdoc update | ||||
| - Fix don't filter out ongoing full day events (#3095) | ||||
| - Fix date not shown when clock in analog mode (#3100) | ||||
| - Fix envcanada today percentage-of-precipitation (#3106) | ||||
| - Fix updatenotification where no branch is checked out but e.g. a version tag (#3130) | ||||
| - Fix yr weather provider after changes in yr API (#3189) | ||||
|  | ||||
| ## [2.23.0] - 2023-04-04 | ||||
|  | ||||
| Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt. | ||||
|  | ||||
| Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added increments for hourly forecasts in weather module (#2996) | ||||
| - Added tests for hourly weather forecast | ||||
| - Added possibility to ignore MagicMirror repo in updatenotification module | ||||
| - Added Pirate Weather as new weather-provider (#3005) | ||||
| - Added possibility to use your own templates in Alert module | ||||
| - Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403) | ||||
| - Added possibility to use environment variables in `config.js` (#1756) | ||||
| - Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed | ||||
| - Added thai language to alert module | ||||
| - Added option `sendNotifications` in clock module (#3056) | ||||
| - Added tests for some weather utils | ||||
|  | ||||
| ### Removed | ||||
|  | ||||
| - Removed darksky weather-provider | ||||
| - Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896) | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Use develop as target branch for dependabot | ||||
| - Update issue template, contributing doc and sample config | ||||
| - The weather modules clearly separates precipitation amount and probability (risk of rain/snow) | ||||
|   - This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`. | ||||
| - Update tests for weather and calendar module | ||||
| - Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit | ||||
| - Update dates in Calendar widgets every minute | ||||
| - Cleanup jest coverage for patches | ||||
| - Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules | ||||
| - Update Eslint config, add new rule and handle issue | ||||
| - Convert lots of callbacks to async/await | ||||
| - Revise require imports (#3071 and #3072) | ||||
| - Use `config.js-old` instead of file with timestamp suffix when backing up config with a `config.template` in use (#3104) | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fix wrong day labels in envcanada forecast (#2987) | ||||
| - Fix for missing default class name prefix for customEvents in calendar | ||||
| - Fix electron flashing white screen on startup (#1919) | ||||
| - Fix weathergov provider hourly forecast (#3008) | ||||
| - Fix message display with HTML code into alert module (#2828) | ||||
| - Fix typo in french translation | ||||
| - Yr wind direction is no longer inverted | ||||
| - Fix async node_helper stopping electron start (#2487) | ||||
| - The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019) | ||||
| - Fix precipitation css styles and rounding value | ||||
| - Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053) | ||||
| - Fix empty news feed stopping the reload forever | ||||
| - Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last | ||||
| - Lint: Use template literals instead of string concatenation | ||||
| - Fix default alert module to render HTML for title and message | ||||
| - Fix Open-Meteo wind speed units | ||||
|  | ||||
| ## [2.22.0] - 2023-01-01 | ||||
|  | ||||
| Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom. | ||||
|  | ||||
| Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you! | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added new calendar options for colored entries and improved styling (#3033) | ||||
| - Added test for remoteFile option in compliments module | ||||
| - Added hourlyWeather functionality to Weather.gov weather-provider | ||||
| - Added css class names "today" and "tomorrow" for default calendar | ||||
| - Added Collaboration.md | ||||
| - Added new github action for dependency review (#2862) | ||||
| - Added a WeatherProvider for Open-Meteo | ||||
| - Added Yr as a weather-provider | ||||
| - Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy" | ||||
| - Added thai language | ||||
| - Added workflow rule to make sure PRs are based against develop | ||||
|  | ||||
| ### Removed | ||||
|  | ||||
| - Removed usage of internal fetch function of node until it is more stable | ||||
| - Removed weatherEndpoint definition from weathergov.js (not used) | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Cleaned up test directory (#2937) and jest config (#2959) | ||||
| - Wait for all modules to start before declaring the system ready (#2487) | ||||
| - Updated e2e tests (moved `done()` in helper functions) and use es6 syntax in all tests | ||||
| - Updated da translation | ||||
| - Rework weather module | ||||
|   - Make sure smhi provider api only gets a maximum of 6 digits coordinates (#2955) | ||||
|   - Use fetch instead of XMLHttpRequest in weather-provider (#2935) | ||||
|   - Reworked how weather-providers handle units (#2849) | ||||
|   - Use unix() method for parsing times, fix suntimes on the way (#2950) | ||||
|   - Refactor conversion functions into utils class (#2958) | ||||
| - The `cors`-method in `server.js` now supports sending and receiving HTTP headers | ||||
| - Replace `…` by `…` | ||||
| - Cleanup compliments module | ||||
| - Updated dependencies including electron to v22 (#2903) | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Correctly show apparent temperature in SMHI weather-provider | ||||
| - Ensure updatenotification module isn't shown when local is _ahead_ of remote | ||||
| - Handle node_helper errors during startup (#2944) | ||||
| - Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works. | ||||
| - Fix cors problems with newsfeed articles (as far as possible), allow disabling cors per feed with option `useCorsProxy: false` (#2840) | ||||
| - Tests not waiting for the application to start and stop before starting the next test | ||||
| - Fix electron tests failing sometimes in github workflow | ||||
| - Fixed gap in clock module when displayed on the left side with displayType=digital | ||||
| - Fixed playwright issue by upgrading to v1.29.1 (#2969) | ||||
|  | ||||
| ## [2.21.0] - 2022-10-01 | ||||
|  | ||||
| Special thanks to: @BKeyport, @buxxi, @davide125, @khassel, @kolbyjack, @krukle, @MikeBishop, @rejas, @sdetweil, @SkySails and @veeck | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added possibility to fetch calendars through socket notifications. | ||||
| - New scripts `install-mm` (and `install-mm:dev`) for simplifying mm installation (now: `npm run install-mm`) and adding params `--no-audit --no-fund --no-update-notifier` for less noise. | ||||
| - New `showTimeToday` option in calendar module shows time for current-day events even if `timeFormat` is `"relative"`. | ||||
| - Added hourly forecasts, apparent temperature & custom location name to SMHI weather-provider. | ||||
| - Added new electron tests for calendar and moved some compliments tests from `e2e` to `electron` because of date mocking, removed mock stuff from compliments module. | ||||
|  | ||||
| ### Removed | ||||
|  | ||||
| - Removed old and deprecated weather modules `currentweather` and `weatherforecast`. | ||||
| - Removed `DAYAFTERTOMORROW` from English. | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Updated dependencies. | ||||
| - Updated jsdoc. | ||||
| - Updated font tree to use variables consistently. | ||||
| - Removed deprecated Docker Repository from issue template. | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Broadcast all calendar events while still honoring global and per-calendar maximumEntries. | ||||
| - Respect rss ttl provided by newsfeed (#2883). | ||||
| - Fix multi day calendar events always presented as "(1/X)" instead of the amount of days the event has progressed. | ||||
| - Fix weatherbit provider to use type config value instead of endpoint. | ||||
| - Fix calendar events which DO NOT specify rrule byday adjusted incorrectly (#2885). | ||||
| - Fix e2e tests not failing on errors (#2911). | ||||
|  | ||||
| ## [2.20.0] - 2022-07-02 | ||||
|  | ||||
| Special thanks to the following contributors: @eouia, @khassel, @kolbyjack, @KristjanESPERANTO, @nathannaveen, @naveensrinivasan, @rejas, @rohitdharavath and @sdetweil. | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added a new config option `httpHeaders` used by helmet (see https://helmetjs.github.io/). You can now set own httpHeaders which will override the defaults in `js/defauls.js` which is useful e.g. if you want to embed MagicMirror into annother website (solves #2847). | ||||
| - Show endDate for calendar events when dateHeader is enabled and showEnd is set to true (#2192). | ||||
| - Added the notification emitting from the weather module on information updated. | ||||
| - Use recommended file extension for YAML files (#2864). | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Use latest node 18 when running tests on github actions. | ||||
| - Updated `electron` to v19 and other dependencies. | ||||
| - Use internal fetch function of node instead external `node-fetch` library if used node version >= `v18`. | ||||
| - Include duplicate events in broadcasts. | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fix problems with non latin fonds caused by updating to fontsource (fixes #2835). | ||||
|  | ||||
| ## [2.19.0] - 2022-04-01 | ||||
|  | ||||
| Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd3rf, @khassel, @kolbyjack, @krekos, @KristjanESPERANTO, @Nerfzooka, @oraclesean, @oscarb, @philnagel, @rejas, @sdetweil, @shin10, @SiderealArt and @Tom-Hirschberger. | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added a config option under the weather module, `absoluteDates`, providing an option to format weather forecast date output with either absolute or relative dates. | ||||
| - Added test for new weather forecast `absoluteDates` property. | ||||
| - The modules get a class hidden added/removed if they get hidden/shown which will also toggle pointer-events. | ||||
| - Added new config option `showTitleAsUrl` to newsfeed module. If set, the displayed title is a link to the article which is useful when running in a browser and you want to read this article. | ||||
| - Added internal cors proxy to get weather-providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too. | ||||
| - Added a WeatherProvider for Weatherflow. | ||||
| - Added new env var `ELECTRON_DISABLE_GPU` which disable gpu under electron if set (fixes #2831). | ||||
| - Added missing Czech translations. | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Deprecated roboto fonts package `roboto-fontface-bower` replaced with `fontsource`. | ||||
| - Updated `electron` to v17, `helmet` to v5 (use defaults of v4) and other dependencies | ||||
| - Updated Font Awesome css class to new default style (fixes #2768) | ||||
| - Replaced deprecated modules `currentweather` and `weatherforecast` with dummy modules only displaying that they have to be replaced. | ||||
| - Include all calendar events from the configured date range when broadcasting. | ||||
| - Updated Danish and German translation. | ||||
| - Updated `node-ical` to v0.15 and added `luxon` as dependency for not breaking the "no-optional" install (see #2718 and #2824). | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Improved and speedup e2e tests, artificial wait after mm start removed. | ||||
| - Improved husky setup not blocking `git commit` if `husky` or `npm` is not installed. | ||||
| - Using a consistent spelling of MagicMirror². | ||||
| - Fix minor console output issue for loading translations (#2814). | ||||
| - Don't adjust startDate for full day events if endDate is in the past. | ||||
| - Fix windspeed conversion error in openweathermap provider. (#2812) | ||||
| - Fix conflicting parms turning off showEnd for full day events. (#2629) | ||||
| - Fix regression, calendar.maximumEntries not used to filter calendar level entries (#2868) | ||||
|  | ||||
| ## [2.18.0] - 2022-01-01 | ||||
|  | ||||
| Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @jupadin, @khassel, @kolbyjack, @KristjanESPERANTO, @MariusVaice, @rejas, @rico24 and @sdetweil. | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added test for calendar recurring event with checks the correct date displayed (related to #2752). | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - ESLint version supports now ECMAScript 2018. | ||||
| - Cleaned up `updatenotification` module and switched to nunjuck template. | ||||
| - Moved calendar tests from category `electron` to `e2e`. | ||||
| - Updated missed translations for Korean language (ko.json). | ||||
| - Updated missed translations for Dutch language (nl.json). | ||||
| - Cleaned up `alert` module and switched to nunjuck template. | ||||
| - Moved weather tests from category `electron` to `e2e`. | ||||
| - Updated github actions. | ||||
| - Replace spectron with playwright, update dependencies including electron update to v16. | ||||
| - Added lithuanian language to translations.js. | ||||
| - Show info message if newsfeed is empty (fixes #2731). | ||||
| - Added dangerouslyDisableAutoEscaping config option for newsfeed templates (fixes #2712). | ||||
| - Added missing shebang to `installers/mm.sh`. | ||||
| - Node versions in templates and github workflows. | ||||
| - Updated translations for Traditional Chinese (Taiwan) (zh-tw.json). | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language. | ||||
| - Fixed `feels_like` data from openweathermaps current weather being ignored (#2678). | ||||
| - Fixed chaotic newsfeed display after network connection loss thanks to @jalibu (#2638). | ||||
| - Fixed incorrect time zone correction of recurring full day events (#2632 and #2634). | ||||
| - Fixed e2e tests by increasing testTimeout. | ||||
| - Revert node-ical update due to missing luxon package. | ||||
| - Fixed User-Agent-Header for newsfeed and calendar module (#2729). | ||||
| - Replace broken shields in Readme and use https for links. | ||||
| - Fixed electron tests with retry. | ||||
| - Fixed Calendar recurring cross timezone error (add/subtract a day, not just offset hours) (#2632). | ||||
| - Fixed Calendar showEnd and Full Date overlay (#2629). | ||||
| - Fixed regression on #2632, #2752. | ||||
| - Broadcast custom symbols in CALENDAR_EVENTS. | ||||
|  | ||||
| ## [2.17.1] - 2021-10-01 | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fixed error when accessing letsencrypt certificates | ||||
| - Fixed Calendar module enhancement: displaying full events without time (#2424) | ||||
|  | ||||
| ## [2.17.0] - 2021-10-01 | ||||
|  | ||||
| Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khassel and @rejas. | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added showTime parameter to clock module for enabling/disabling time display in analog clock. | ||||
| - Added custom electron switches from user config (`config.electronSwitches`). | ||||
| - Added unit tests for updatenotification module. | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Bump electron to v13 (and spectron to v15) and update other dependencies in package.json. | ||||
| - Refactor test configs, use default test config for all tests. | ||||
| - Updated github templates. | ||||
| - Actually test all js and css files when lint script is run. | ||||
| - Updated jsdocs and print warnings during testing too. | ||||
| - Updated weathergov provider to try fetching not just current, but also foreacst, when API URLs available. | ||||
| - Refactored clock layout. | ||||
| - Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime). | ||||
| - Use of `logger.js` in jest tests. | ||||
| - Run prettier over all relevant files. | ||||
| - Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests. | ||||
| - Updated dependencies in package.json. | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fix undefined error with ignoreToday option in weather module (#2620). | ||||
| - Fix time zone correction in calendar module when the date hour is equal to the time zone correction value (#2632). | ||||
| - Fix black cursor on startup when using electron. | ||||
| - Fix update notification not working for own repository (#2644). | ||||
|  | ||||
| ## [2.16.0] - 2021-07-01 | ||||
|  | ||||
| Special thanks to the following contributors: @210954, @B1gG, @codac, @Crazylegstoo, @daniel, @earlman, @ezeholz, @FrancoisRmn, @jupadin, @khassel, @KristjanESPERANTO, @njwilliams, @oemel09, @r3wald, @rejas, @rico24, Faizan Ahmed. | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - Added French translations for "MODULE_CONFIG_ERROR" and "PRECIP". | ||||
| - Added German translation for "PRECIP". | ||||
| - Added Dutch translation for "WEEK", "PRECIP", "MODULE_CONFIG_CHANGED" and "MODULE_CONFIG_ERROR". | ||||
| - Added first test for Alert module. | ||||
| - Added support for `dateFormat` when not using `timeFormat: "absolute"`. | ||||
| - Added custom-properties for colors and fonts for improved styling experience, see `custom.css.sample` file. | ||||
| - Added custom-properties for gaps around body and between modules. | ||||
| - Added test case for recurring calendar events. | ||||
| - Added new Environment Canada provider for default WEATHER module (weather data for Canadian locations only). | ||||
| - Added list view for newsfeed module. | ||||
| - Added dev dependency jest, switching from mocha to jest. | ||||
|  | ||||
| ### Updated | ||||
|  | ||||
| - Bump node-ical to v0.13.0 (now last runtime dependency using deprecated `request` package is removed). | ||||
| - Use codecov in informational mode. | ||||
| - Refactor code into es6 where possible (e.g. var -> let/const). | ||||
| - Use node v16 in github workflow (replacing node v10). | ||||
| - Moved some files into better suited directories. | ||||
| - Updated dependencies in package.json, require node >= v12, remove `rrule-alt` and `rrule`. | ||||
| - Updated dependencies in package.json and migrate husky to v6, fix husky setup in prod environment. | ||||
| - Cleaned up error handling in newsfeed and calendar modules for real. | ||||
| - Updated default WEATHER module such that a provider can optionally set a custom unit-of-measure for precipitation (`weatherObject.precipitationUnits`). | ||||
| - Updated documentation. | ||||
| - Updated jest tests: Reset changes on js/logger.js, mock logger.js in global_vars tests. | ||||
| - Updated dependencies in package.json. | ||||
|  | ||||
| ### Removed | ||||
|  | ||||
| - Switching from mocha to jest so removed following dev dependencies: chai, chai-as-promised, mocha, mocha-each, mocha-logger. | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fix calendar start function logging inconsistency. | ||||
| - Fix updatenotification start function logging inconsistency. | ||||
| - Checks and applies the showDescription setting for the newsfeed module again. | ||||
| - Fix issue with openweathermap not showing current or forecast info when using onecall API. | ||||
| - Fix tests in weather module and add one for decimalPoint in forecast. | ||||
| - Fix decimalSymbol in the forecast part of the new weather module (#2530). | ||||
| - Fix wrong treatment of `appendLocationNameToHeader` when using `ukmetofficedatahub`. | ||||
| - Fix alert not recognizing multiple alerts (#2522). | ||||
| - Fix fetch option httpsAgent to agent in calendar module (#466). | ||||
| - Fix module updatenotification which did not work for repos with many refs (#1907). | ||||
| - Fix config check failing when encountering let syntax ("Parsing error: Unexpected token config"). | ||||
| - Fix calendar debug check. | ||||
| - Really run prettier over all files. | ||||
| - Fix logger.js after jest changes, use --forceExit running jest. | ||||
| - Workaround for dev_console test using getWindowCount. | ||||
|  | ||||
| ## [2.15.0] - 2021-04-01 | ||||
|  | ||||
| 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 | ||||
|  | ||||
| @@ -15,26 +518,24 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, | ||||
|  | ||||
| - Added new log level "debug" to the logger. | ||||
| - Added new parameter "useKmh" to weather module for displaying wind speed as kmh. | ||||
| - Chuvash translation. | ||||
| - 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. | ||||
| - Calendar: new options "limitDays" and "coloredEvents". | ||||
| - 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. | ||||
| - Added GitHub workflows for automated testing and changelog enforcement. | ||||
|  | ||||
| ### 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. | ||||
| - Updated dependencies to latest versions. | ||||
| - Updated dependencies eslint, feedme, simple-git and socket.io to latest versions. | ||||
| - Updated lithuanian translation. | ||||
| - Updated config sample. | ||||
| - Highlight required version mismatch. | ||||
| - No select Text for TouchScreen use. | ||||
| - Corrected logic for timeFormat "relative" and "absolute". | ||||
| @@ -44,7 +545,7 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, | ||||
|  | ||||
| ### Deleted | ||||
|  | ||||
| - Removed Travis CI intergration. | ||||
| - Removed Travis CI integration. | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| @@ -55,19 +556,19 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, | ||||
| - Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155) | ||||
| - Add a space after icons of sunrise and sunset. (#2169) | ||||
| - Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177) | ||||
| - Fix windspeed convertion error in ukmetoffice weather provider. (#2189) | ||||
| - Fix windspeed conversion error in ukmetoffice weather-provider. (#2189) | ||||
| - Fix console.debug not having timestamps. (#2199) | ||||
| - Fix calendar full day event east of UTC start time. (#2200) | ||||
| - Fix non-fullday recurring rule processing. (#2216) | ||||
| - Catch errors when parsing calendar data with ical. (#2022) | ||||
| - Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228) | ||||
| - Weather module - Always displays night icons when local is other then English. (#2221) | ||||
| - Update Node-ical 0.12.4 , fix invalid RRULE format in cal entries | ||||
| - Weather module - Always displays night icons when local is other than English. (#2221) | ||||
| - Updated node-ical 0.12.4, fix invalid RRULE format in cal entries | ||||
| - Fix package.json for optional electron dependency (2378) | ||||
| - Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379) | ||||
| - Updated node-ical version again, 0.12.5, change RRULE fix (#2371, #2379) | ||||
| - Remove undefined objects from modules array (#2382) | ||||
| - Update node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch) | ||||
| - Update simple-git version to 2.31 unhandled promise rejection (#2383) | ||||
| - Updated node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch) | ||||
| - Updated simple-git version to 2.31 unhandled promise rejection (#2383) | ||||
|  | ||||
| ## [2.13.0] - 2020-10-01 | ||||
|  | ||||
| @@ -77,11 +578,11 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura | ||||
|  | ||||
| ### Added | ||||
|  | ||||
| - `--dry-run` option adde 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. | ||||
| - `--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`. | ||||
| - Add lithuanian language. | ||||
| - 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. | ||||
| @@ -145,14 +646,14 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan | ||||
| - 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 latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926) | ||||
| - 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 🚨 | ||||
|  | ||||
| In the past years the project has grown a lot. This came with a huge downside: poor maintainability. If I let the project continue the way it was, it would eventually crash and burn. More important: I would completely lose the drive and interest to continue the project. Because of this the decision was made to simplify the core by removing all side features like automatic installers and support for exotic platforms. This release (2.11.0) is the first real release that will reflect (parts) of these changes. As a result of this, some things might break. So before you continue make sure to backup your installation. Your config, your modules or better yet: your full MagicMirror folder. In other words: update at your own risk. | ||||
| In the past years the project has grown a lot. This came with a huge downside: poor maintainability. If I let the project continue the way it was, it would eventually crash and burn. More important: I would completely lose the drive and interest to continue the project. Because of this the decision was made to simplify the core by removing all side features like automatic installers and support for exotic platforms. This release (2.11.0) is the first real release that will reflect (parts) of these changes. As a result of this, some things might break. So before you continue make sure to backup your installation. Your config, your modules or better yet: your full MagicMirror² folder. In other words: update at your own risk. | ||||
|  | ||||
| For more information regarding this major change, please check issue [#1860](https://github.com/MichMich/MagicMirror/issues/1860). | ||||
|  | ||||
| @@ -284,7 +785,7 @@ Special thanks to @sdetweil for all his great contributions! | ||||
| - Use Feels Like temp from feed if present | ||||
| - Optionally display probability of precipitation (PoP) in current weather (UK Met Office data) | ||||
| - Automatically try to fix eslint errors by passing `--fix` option to it | ||||
| - Added sunrise and sunset times to weathergov weather provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705) | ||||
| - Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705) | ||||
| - Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned | ||||
| - Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc | ||||
|  | ||||
| @@ -292,9 +793,10 @@ Special thanks to @sdetweil for all his great contributions! | ||||
|  | ||||
| - English translation for "Feels" to "Feels like" | ||||
| - Fixed the example calendar url in `config.js.sample` | ||||
| - Update `ical.js` to solve various calendar issues. | ||||
| - Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676) | ||||
| - Updated `ical.js` to solve various calendar issues. | ||||
| - Updated weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676) | ||||
| - Only update clock once per minute when seconds aren't shown | ||||
| - Updated weather-provider documentation. | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| @@ -314,7 +816,7 @@ Special thanks to @sdetweil for all his great contributions! | ||||
| - use current username vs hardcoded 'pi' to support non-pi install | ||||
| - check for npm installed. node install doesn't do npm anymore | ||||
| - check for mac as part of PM2 install, add install option string | ||||
| - update pm2 config with current username instead of hard coded 'pi' | ||||
| - Updated pm2 config with current username instead of hard coded 'pi' | ||||
| - check for screen saver config, "/etc/xdg/lxsession", bypass if not setup | ||||
|  | ||||
| ## [2.7.1] - 2019-04-02 | ||||
| @@ -352,7 +854,7 @@ Fixed `package.json` version number. | ||||
| - Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285). | ||||
| - Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504). | ||||
| - Fixed analogue clock border display issue where non-black backgrounds used (previous fix for issue 611) | ||||
| - Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims). | ||||
| - Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror² now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims). | ||||
| - Installation script problems with raspbian | ||||
| - Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MichMich/MagicMirror/pull/1534) | ||||
| - Calendar: Fix exdate handling when multiple values are specified (comma separated) | ||||
| @@ -376,7 +878,7 @@ Fixed `package.json` version number. | ||||
| - Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516) | ||||
| - Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538) | ||||
| - Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545) | ||||
| - Added weather.gov as a new weather provider for US locations | ||||
| - Added weather.gov as a new weather-provider for US locations | ||||
|  | ||||
| ## [2.6.0] - 2019-01-01 | ||||
|  | ||||
| @@ -439,7 +941,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
| ### 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) | ||||
| @@ -464,7 +966,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
|  | ||||
| ## [2.4.0] - 2018-07-01 | ||||
|  | ||||
| ⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror Wiki](https://github.com/michmich/magicmirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage). | ||||
| ⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror² Wiki](https://github.com/michmich/magicmirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage). | ||||
|  | ||||
| ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install` | ||||
|  | ||||
| @@ -522,7 +1024,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
| - Add types for module. | ||||
| - Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR. | ||||
| - Allow scrolling in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures) | ||||
| - Changed 'compliments.js' - update DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments | ||||
| - Changed 'compliments.js' - Updated DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments | ||||
| - Automated unit tests utils, deprecated, translator, cloneObject(lockstrings) | ||||
| - Automated integration tests translations | ||||
| - Add advanced filtering to the excludedEvents configuration of the default calendar module | ||||
| @@ -534,7 +1036,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
|  | ||||
| - Add link to GitHub repository which contains the respective Dockerfile. | ||||
| - Optimized automated unit tests cloneObject, cmpVersions | ||||
| - Update notifications use now translation templates instead of normal strings. | ||||
| - Updated notifications use now translation templates instead of normal strings. | ||||
| - Yarn can be used now as an installation tool | ||||
| - Changed Electron dependency to v1.7.13. | ||||
|  | ||||
| @@ -705,7 +1207,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
| - Add use pm2 for manager process into Installer RaspberryPi script. | ||||
| - Russian Translation. | ||||
| - Afrikaans Translation. | ||||
| - Add postinstall script to notify user that MagicMirror installed successfully despite warnings from NPM. | ||||
| - Add postinstall script to notify user that MagicMirror² installed successfully despite warnings from NPM. | ||||
| - Init tests using mocha. | ||||
| - Option to use RegExp in Calendar's titleReplace. | ||||
| - Hungarian Translation. | ||||
| @@ -741,7 +1243,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Update .gitignore to not ignore default modules folder. | ||||
| - Updated .gitignore to not ignore default modules folder. | ||||
| - Remove white flash on boot up. | ||||
| - Added `update` in Raspberry Pi installation script. | ||||
| - Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MichMich/MagicMirror/issues/611)) | ||||
| @@ -768,7 +1270,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
| - Add VSCode IntelliSense support. | ||||
| - Module API: Add Visibility locking to module system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#visibility-locking) for more information. | ||||
| - Module API: Method to overwrite the module's header. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#getheader) for more information. | ||||
| - Module API: Option to define the minimum MagicMirror version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information. | ||||
| - Module API: Option to define the minimum MagicMirror² version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information. | ||||
| - Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information. | ||||
| - Possibility to use the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information. | ||||
| - Added option to show rain amount in the weatherforecast default module | ||||
| @@ -826,8 +1328,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we | ||||
| ### Updated | ||||
|  | ||||
| - Force fullscreen when kioskmode is active. | ||||
| - Update the .github templates and information with more modern information. | ||||
| - Update the Gruntfile with a more functional StyleLint implementation. | ||||
| - Updated the .github templates and information with more modern information. | ||||
| - Updated the Gruntfile with a more functional StyleLint implementation. | ||||
|  | ||||
| ## [2.0.4] - 2016-08-07 | ||||
|  | ||||
| @@ -915,6 +1417,6 @@ It includes (but is not limited to) the following features: | ||||
|  | ||||
| ## [1.0.0] - 2014-02-16 | ||||
|  | ||||
| ### Initial release of MagicMirror. | ||||
| ### Initial release of MagicMirror | ||||
|  | ||||
| This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the) | ||||
|   | ||||
							
								
								
									
										18
									
								
								Collaboration.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Collaboration.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| This document describes how collaborators of this repository should work together. | ||||
|  | ||||
| ## Pull Requests | ||||
|  | ||||
| - never merge your own PR's | ||||
| - never merge without someone having approved (approving and merging from same person is allowed) | ||||
| - wait for all approvals requested (or the author decides something different in the comments) | ||||
| - never merge to `master`, except for releases (because of update notification) | ||||
| - merges to master should be tagged with the "mastermerge" label so that the test runs through | ||||
|  | ||||
| ## Issues | ||||
|  | ||||
| - "real" Issues are closed if the problem is solved and the fix is released | ||||
| - unrelated Issues (e.g. related to a foreign module) are closed immediately with a comment to open an issue in the module repository or to discuss this further in the forum or discord | ||||
|  | ||||
| ## Releases | ||||
|  | ||||
| - are done by @MichMich only | ||||
| @@ -1,6 +1,6 @@ | ||||
| # The MIT License (MIT) | ||||
|  | ||||
| Copyright © 2016-2020 Michael Teeuw | ||||
| Copyright © 2016-2022 Michael Teeuw | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person | ||||
| obtaining a copy of this software and associated documentation | ||||
|   | ||||
							
								
								
									
										30
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,11 +1,17 @@ | ||||
|  | ||||
|  | ||||
| <p 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="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 style="text-align: center"> | ||||
|   <a href="https://choosealicense.com/licenses/mit"> | ||||
| 		<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"> | ||||
| 	</a> | ||||
| 	<img src="https://img.shields.io/github/actions/workflow/status/michmich/magicmirror/automated-tests.yaml" alt="GitHub Actions"> | ||||
| 	<img src="https://img.shields.io/github/checks-status/michmich/magicmirror/master" alt="Build Status"> | ||||
| 	<a href="https://codecov.io/gh/MichMich/MagicMirror"> | ||||
| 		<img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/> | ||||
| 	</a> | ||||
| 	<a href="https://github.com/MichMich/MagicMirror"> | ||||
| 		<img src="https://img.shields.io/github/stars/michmich/magicmirror?style=social"> | ||||
| 	</a> | ||||
| </p> | ||||
|  | ||||
| **MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors). | ||||
| @@ -21,13 +27,20 @@ For the full documentation including **[installation instructions](https://docs. | ||||
| - Website: [https://magicmirror.builders](https://magicmirror.builders) | ||||
| - Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders) | ||||
| - Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders) | ||||
|   - Technical discussions: https://forum.magicmirror.builders/category/11/core-system | ||||
| - Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx) | ||||
| - Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror) | ||||
| - Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate) | ||||
|  | ||||
| ## 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/about/contributing.html](https://docs.magicmirror.builders/about/contributing.html) | ||||
|  | ||||
| ## Enjoying MagicMirror? Consider a donation! | ||||
|  | ||||
| @@ -38,7 +51,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,7 +2,7 @@ | ||||
|  | ||||
| // Use separate scope to prevent global scope pollution | ||||
| (function () { | ||||
| 	var config = {}; | ||||
| 	const config = {}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Helper function to get server address/hostname from either the commandline or env | ||||
| @@ -11,15 +11,13 @@ | ||||
| 		/** | ||||
| 		 * Get command line parameters | ||||
| 		 * Assumes that a cmdline parameter is defined with `--key [value]` | ||||
| 		 * | ||||
| 		 * @param {string} key key to look for at the command line | ||||
| 		 * @param {string} defaultValue value if no key is given at the command line | ||||
| 		 * | ||||
| 		 * @returns {string} the value of the parameter | ||||
| 		 */ | ||||
| 		function getCommandLineParameter(key, defaultValue = undefined) { | ||||
| 			var index = process.argv.indexOf(`--${key}`); | ||||
| 			var value = index > -1 ? process.argv[index + 1] : undefined; | ||||
| 			const index = process.argv.indexOf(`--${key}`); | ||||
| 			const value = index > -1 ? process.argv[index + 1] : undefined; | ||||
| 			return value !== undefined ? String(value) : defaultValue; | ||||
| 		} | ||||
|  | ||||
| @@ -34,9 +32,7 @@ | ||||
|  | ||||
| 	/** | ||||
| 	 * Gets the config from the specified server url | ||||
| 	 * | ||||
| 	 * @param {string} url location where the server is running. | ||||
| 	 * | ||||
| 	 * @returns {Promise} the config | ||||
| 	 */ | ||||
| 	function getServerConfig(url) { | ||||
| @@ -45,7 +41,7 @@ | ||||
| 			// Select http or https module, depending on requested url | ||||
| 			const lib = url.startsWith("https") ? require("https") : require("http"); | ||||
| 			const request = lib.get(url, (response) => { | ||||
| 				var configData = ""; | ||||
| 				let configData = ""; | ||||
|  | ||||
| 				// Gather incoming data | ||||
| 				response.on("data", function (chunk) { | ||||
| @@ -65,8 +61,7 @@ | ||||
|  | ||||
| 	/** | ||||
| 	 * Print a message to the console in case of errors | ||||
| 	 * | ||||
| 	 * @param {string} [message] error message to print | ||||
| 	 * @param {string} message error message to print | ||||
| 	 * @param {number} code error code for the exit call | ||||
| 	 */ | ||||
| 	function fail(message, code = 1) { | ||||
| @@ -81,15 +76,16 @@ | ||||
| 	getServerAddress(); | ||||
|  | ||||
| 	(config.address && config.port) || fail(); | ||||
| 	var prefix = config.tls ? "https://" : "http://"; | ||||
| 	const prefix = config.tls ? "https://" : "http://"; | ||||
|  | ||||
| 	// Only start the client if a non-local server was provided | ||||
| 	if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) { | ||||
| 		getServerConfig(`${prefix}${config.address}:${config.port}/config/`) | ||||
| 			.then(function (configReturn) { | ||||
| 				// Pass along the server config via an environment variable | ||||
| 				var env = Object.create(process.env); | ||||
| 				var options = { env: env }; | ||||
| 				const env = Object.create(process.env); | ||||
| 				env.clientonly = true; // set to pass to electron.js | ||||
| 				const options = { env: env }; | ||||
| 				configReturn.address = config.address; | ||||
| 				configReturn.port = config.port; | ||||
| 				configReturn.tls = config.tls; | ||||
|   | ||||
| @@ -1,23 +1,26 @@ | ||||
| /* Magic Mirror Config Sample | ||||
| /* MagicMirror² Config Sample | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  * | ||||
|  * For more information on how you can configure this file | ||||
|  * See https://github.com/MichMich/MagicMirror#configuration | ||||
|  * see https://docs.magicmirror.builders/configuration/introduction.html | ||||
|  * and https://docs.magicmirror.builders/modules/configuration.html | ||||
|  * | ||||
|  * You can use environment variables using a `config.js.template` file instead of `config.js` | ||||
|  * which will be converted to `config.js` while starting. For more information | ||||
|  * see https://docs.magicmirror.builders/configuration/introduction.html#enviromnent-variables | ||||
|  */ | ||||
|  | ||||
| var config = { | ||||
| 	address: "localhost", 	// Address to listen on, can be: | ||||
| let config = { | ||||
| 	address: "localhost",	// Address to listen on, can be: | ||||
| 							// - "localhost", "127.0.0.1", "::1" to listen on loopback interface | ||||
| 							// - another specific IPv4/6 to listen on a specific interface | ||||
| 							// - "0.0.0.0", "::" to listen on any interface | ||||
| 							// Default, when address config is left out or empty, is "localhost" | ||||
| 	port: 8080, | ||||
| 	basePath: "/", 	// The URL path where MagicMirror is hosted. If you are using a Reverse proxy | ||||
| 					// 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 | ||||
| 	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 : | ||||
| @@ -28,14 +31,10 @@ var config = { | ||||
| 	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 | ||||
|  | ||||
| 	modules: [ | ||||
| 		{ | ||||
| @@ -56,8 +55,10 @@ var config = { | ||||
| 			config: { | ||||
| 				calendars: [ | ||||
| 					{ | ||||
| 						fetchInterval: 7 * 24 * 60 * 60 * 1000, | ||||
| 						symbol: "calendar-check", | ||||
| 						url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"					} | ||||
| 						url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics" | ||||
| 					} | ||||
| 				] | ||||
| 			} | ||||
| 		}, | ||||
| @@ -66,22 +67,26 @@ var config = { | ||||
| 			position: "lower_third" | ||||
| 		}, | ||||
| 		{ | ||||
| 			module: "currentweather", | ||||
| 			module: "weather", | ||||
| 			position: "top_right", | ||||
| 			config: { | ||||
| 				weatherProvider: "openweathermap", | ||||
| 				type: "current", | ||||
| 				location: "New York", | ||||
| 				locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city | ||||
| 				appid: "YOUR_OPENWEATHER_API_KEY" | ||||
| 				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" | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
|   | ||||
							
								
								
									
										31
									
								
								css/custom.css.sample
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								css/custom.css.sample
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| /* MagicMirror² Custom CSS Sample | ||||
|  * | ||||
|  * Change color and fonts here. | ||||
|  * | ||||
|  * Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;' | ||||
|  * | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
|  | ||||
| /* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */ | ||||
| /* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;300;400;700&display=swap'); */ | ||||
|  | ||||
| :root { | ||||
|   --color-text: #999; | ||||
|   --color-text-dimmed: #666; | ||||
|   --color-text-bright: #fff; | ||||
|   --color-background: black; | ||||
|  | ||||
|   --font-primary: "Roboto Condensed"; | ||||
|   --font-secondary: "Roboto"; | ||||
|    | ||||
|   --font-size: 20px; | ||||
|   --font-size-small: 0.75rem; | ||||
|  | ||||
|   --gap-body-top: 60px; | ||||
|   --gap-body-right: 60px; | ||||
|   --gap-body-bottom: 60px; | ||||
|   --gap-body-left: 60px; | ||||
|    | ||||
|   --gap-modules: 30px; | ||||
| } | ||||
							
								
								
									
										117
									
								
								css/main.css
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								css/main.css
									
									
									
									
									
								
							| @@ -1,8 +1,29 @@ | ||||
| :root { | ||||
|   --color-text: #999; | ||||
|   --color-text-dimmed: #666; | ||||
|   --color-text-bright: #fff; | ||||
|   --color-background: #000; | ||||
|   --font-primary: "Roboto Condensed"; | ||||
|   --font-secondary: "Roboto"; | ||||
|   --font-size: 20px; | ||||
|   --font-size-xsmall: 0.75rem; | ||||
|   --font-size-small: 1rem; | ||||
|   --font-size-medium: 1.5rem; | ||||
|   --font-size-large: 3.25rem; | ||||
|   --font-size-xlarge: 3.75rem; | ||||
|   --gap-body-top: 60px; | ||||
|   --gap-body-right: 60px; | ||||
|   --gap-body-bottom: 60px; | ||||
|   --gap-body-left: 60px; | ||||
|   --gap-modules: 30px; | ||||
| } | ||||
|  | ||||
| html { | ||||
|   cursor: none; | ||||
|   overflow: hidden; | ||||
|   background: #000; | ||||
|   background: var(--color-background); | ||||
|   user-select: none; | ||||
|   font-size: var(--font-size); | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar { | ||||
| @@ -10,16 +31,15 @@ html { | ||||
| } | ||||
|  | ||||
| body { | ||||
|   margin: 60px; | ||||
|   margin: var(--gap-body-top) var(--gap-body-right) var(--gap-body-bottom) var(--gap-body-left); | ||||
|   position: absolute; | ||||
|   height: calc(100% - 120px); | ||||
|   width: calc(100% - 120px); | ||||
|   background: #000; | ||||
|   color: #aaa; | ||||
|   font-family: "Roboto Condensed", sans-serif; | ||||
|   height: calc(100% - var(--gap-body-top) - var(--gap-body-bottom)); | ||||
|   width: calc(100% - var(--gap-body-right) - var(--gap-body-left)); | ||||
|   background: var(--color-background); | ||||
|   color: var(--color-text); | ||||
|   font-family: var(--font-primary), sans-serif; | ||||
|   font-weight: 400; | ||||
|   font-size: 2em; | ||||
|   line-height: 1.5em; | ||||
|   line-height: 1.5; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
| } | ||||
|  | ||||
| @@ -28,60 +48,60 @@ body { | ||||
|  */ | ||||
|  | ||||
| .dimmed { | ||||
|   color: #666; | ||||
|   color: var(--color-text-dimmed); | ||||
| } | ||||
|  | ||||
| .normal { | ||||
|   color: #999; | ||||
|   color: var(--color-text); | ||||
| } | ||||
|  | ||||
| .bright { | ||||
|   color: #fff; | ||||
|   color: var(--color-text-bright); | ||||
| } | ||||
|  | ||||
| .xsmall { | ||||
|   font-size: 15px; | ||||
|   line-height: 20px; | ||||
|   font-size: var(--font-size-xsmall); | ||||
|   line-height: 1.275; | ||||
| } | ||||
|  | ||||
| .small { | ||||
|   font-size: 20px; | ||||
|   line-height: 25px; | ||||
|   font-size: var(--font-size-small); | ||||
|   line-height: 1.25; | ||||
| } | ||||
|  | ||||
| .medium { | ||||
|   font-size: 30px; | ||||
|   line-height: 35px; | ||||
|   font-size: var(--font-size-medium); | ||||
|   line-height: 1.225; | ||||
| } | ||||
|  | ||||
| .large { | ||||
|   font-size: 65px; | ||||
|   line-height: 65px; | ||||
|   font-size: var(--font-size-large); | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| .xlarge { | ||||
|   font-size: 75px; | ||||
|   line-height: 75px; | ||||
|   font-size: var(--font-size-xlarge); | ||||
|   line-height: 1; | ||||
|   letter-spacing: -3px; | ||||
| } | ||||
|  | ||||
| .thin { | ||||
|   font-family: Roboto, sans-serif; | ||||
|   font-family: var(--font-secondary), sans-serif; | ||||
|   font-weight: 100; | ||||
| } | ||||
|  | ||||
| .light { | ||||
|   font-family: "Roboto Condensed", sans-serif; | ||||
|   font-family: var(--font-primary), sans-serif; | ||||
|   font-weight: 300; | ||||
| } | ||||
|  | ||||
| .regular { | ||||
|   font-family: "Roboto Condensed", sans-serif; | ||||
|   font-family: var(--font-primary), sans-serif; | ||||
|   font-weight: 400; | ||||
| } | ||||
|  | ||||
| .bold { | ||||
|   font-family: "Roboto Condensed", sans-serif; | ||||
|   font-family: var(--font-primary), sans-serif; | ||||
|   font-weight: 700; | ||||
| } | ||||
|  | ||||
| @@ -95,14 +115,14 @@ body { | ||||
|  | ||||
| header { | ||||
|   text-transform: uppercase; | ||||
|   font-size: 15px; | ||||
|   font-family: "Roboto Condensed", Arial, Helvetica, sans-serif; | ||||
|   font-size: var(--font-size-xsmall); | ||||
|   font-family: var(--font-primary), Arial, Helvetica, sans-serif; | ||||
|   font-weight: 400; | ||||
|   border-bottom: 1px solid #666; | ||||
|   border-bottom: 1px solid var(--color-text-dimmed); | ||||
|   line-height: 15px; | ||||
|   padding-bottom: 5px; | ||||
|   margin-bottom: 10px; | ||||
|   color: #999; | ||||
|   color: var(--color-text); | ||||
| } | ||||
|  | ||||
| sup { | ||||
| @@ -115,11 +135,19 @@ sup { | ||||
|  */ | ||||
|  | ||||
| .module { | ||||
|   margin-bottom: 30px; | ||||
|   margin-bottom: var(--gap-modules); | ||||
| } | ||||
|  | ||||
| .module.hidden { | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .module:not(.hidden) { | ||||
|   pointer-events: auto; | ||||
| } | ||||
|  | ||||
| .region.bottom .module { | ||||
|   margin-top: 30px; | ||||
|   margin-top: var(--gap-modules); | ||||
|   margin-bottom: 0; | ||||
| } | ||||
|  | ||||
| @@ -143,17 +171,10 @@ sup { | ||||
|  | ||||
| .region.fullscreen { | ||||
|   position: absolute; | ||||
|   top: -60px; | ||||
|   left: -60px; | ||||
|   right: -60px; | ||||
|   bottom: -60px; | ||||
|   inset: calc(-1 * var(--gap-body-top)) calc(-1 * var(--gap-body-right)) calc(-1 * var(--gap-body-bottom)) calc(-1 * var(--gap-body-left)); | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .region.fullscreen * { | ||||
|   pointer-events: auto; | ||||
| } | ||||
|  | ||||
| .region.right { | ||||
|   right: 0; | ||||
|   text-align: right; | ||||
| @@ -163,18 +184,6 @@ sup { | ||||
|   top: 0; | ||||
| } | ||||
|  | ||||
| .region.top .container { | ||||
|   margin-bottom: 25px; | ||||
| } | ||||
|  | ||||
| .region.bottom .container { | ||||
|   margin-top: 25px; | ||||
| } | ||||
|  | ||||
| .region.top .container:empty { | ||||
|   margin-bottom: 0; | ||||
| } | ||||
|  | ||||
| .region.top.center, | ||||
| .region.bottom.center { | ||||
|   left: 50%; | ||||
| @@ -191,10 +200,6 @@ sup { | ||||
|   bottom: 0; | ||||
| } | ||||
|  | ||||
| .region.bottom .container:empty { | ||||
|   margin-top: 0; | ||||
| } | ||||
|  | ||||
| .region.bottom.right, | ||||
| .region.bottom.center, | ||||
| .region.bottom.left { | ||||
|   | ||||
| @@ -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."); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										45
									
								
								fonts/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										45
									
								
								fonts/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,12 +1,37 @@ | ||||
| { | ||||
|   "name": "magicmirror-fonts", | ||||
|   "requires": true, | ||||
|   "lockfileVersion": 1, | ||||
|   "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==" | ||||
|     } | ||||
|   } | ||||
| 	"name": "magicmirror-fonts", | ||||
| 	"lockfileVersion": 2, | ||||
| 	"requires": true, | ||||
| 	"packages": { | ||||
| 		"": { | ||||
| 			"name": "magicmirror-fonts", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@fontsource/roboto": "^5.0.8", | ||||
| 				"@fontsource/roboto-condensed": "^5.0.8" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@fontsource/roboto": { | ||||
| 			"version": "5.0.8", | ||||
| 			"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz", | ||||
| 			"integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA==" | ||||
| 		}, | ||||
| 		"node_modules/@fontsource/roboto-condensed": { | ||||
| 			"version": "5.0.8", | ||||
| 			"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.8.tgz", | ||||
| 			"integrity": "sha512-xAXYY+ys24OZ/eOfXJZILPu2xOB7c0ZruM4cd4TSzX3WGj4dZbXYwCEowLldKbZye6LTqiltpFLP/g/Ne0qGLg==" | ||||
| 		} | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@fontsource/roboto": { | ||||
| 			"version": "5.0.8", | ||||
| 			"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz", | ||||
| 			"integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA==" | ||||
| 		}, | ||||
| 		"@fontsource/roboto-condensed": { | ||||
| 			"version": "5.0.8", | ||||
| 			"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.8.tgz", | ||||
| 			"integrity": "sha512-xAXYY+ys24OZ/eOfXJZILPu2xOB7c0ZruM4cd4TSzX3WGj4dZbXYwCEowLldKbZye6LTqiltpFLP/g/Ne0qGLg==" | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
| 	"name": "magicmirror-fonts", | ||||
| 	"description": "Package for fonts use by MagicMirror Core.", | ||||
| 	"description": "Package for fonts use by MagicMirror² Core.", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "git+https://github.com/MichMich/MagicMirror.git" | ||||
| @@ -10,6 +10,7 @@ | ||||
| 		"url": "https://github.com/MichMich/MagicMirror/issues" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"roboto-fontface": "^0.10.0" | ||||
| 		"@fontsource/roboto": "^5.0.8", | ||||
| 		"@fontsource/roboto-condensed": "^5.0.8" | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										665
									
								
								fonts/roboto.css
									
									
									
									
									
								
							
							
						
						
									
										665
									
								
								fonts/roboto.css
									
									
									
									
									
								
							| @@ -1,58 +1,671 @@ | ||||
| /* roboto-cyrillic-ext-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 100; | ||||
|   src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff"); | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-100-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: local("Roboto Condensed Light"), local("RobotoCondensed-Light"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"), | ||||
|     url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local("Roboto Condensed"), local("RobotoCondensed-Regular"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"), | ||||
|     url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-weight: 700; | ||||
|   src: local("Roboto Condensed Bold"), local("RobotoCondensed-Bold"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"), | ||||
|     url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff"); | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local("Roboto"), local("Roboto-Regular"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff"); | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 500; | ||||
|   src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff"); | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff"); | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-weight: 300; | ||||
|   src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff"); | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-vietnamese-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-vietnamese-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-vietnamese-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: var(--fontsource-display, swap); | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff2") format("woff2"), | ||||
|     url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|   | ||||
							
								
								
									
										105
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								index.html
									
									
									
									
									
								
							| @@ -1,55 +1,60 @@ | ||||
| <!DOCTYPE html> | ||||
| <!doctype html> | ||||
| <html> | ||||
| <head> | ||||
| 	<title>MagicMirror²</title> | ||||
| 	<meta name="google" content="notranslate" /> | ||||
| 	<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> | ||||
|   <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" /> | ||||
|     <link rel="stylesheet" type="text/css" href="vendor/node_modules/animate.css/animate.min.css" /> | ||||
|     <!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. --> | ||||
|  | ||||
| 	<script type="text/javascript"> | ||||
| 		var version = "#VERSION#"; | ||||
| 	</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="modules/default/utils.js"></script> | ||||
|     <script type="text/javascript" src="js/logger.js"></script> | ||||
|     <script type="text/javascript" src="translations/translations.js"></script> | ||||
|     <script type="text/javascript" src="js/translator.js"></script> | ||||
|     <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/animateCSS.js"></script> | ||||
|     <script type="text/javascript" src="js/main.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| #!/bin/bash | ||||
| # This file is still here to keep PM2 working on older installations. | ||||
| cd ~/MagicMirror | ||||
| DISPLAY=:0 npm start | ||||
|   | ||||
							
								
								
									
										33
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| module.exports = async () => { | ||||
| 	return { | ||||
| 		verbose: true, | ||||
| 		testTimeout: 20000, | ||||
| 		testSequencer: "<rootDir>/tests/utils/test_sequencer.js", | ||||
| 		projects: [ | ||||
| 			{ | ||||
| 				displayName: "unit", | ||||
| 				globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js", | ||||
| 				moduleNameMapper: { | ||||
| 					logger: "<rootDir>/js/logger.js" | ||||
| 				}, | ||||
| 				testMatch: ["**/tests/unit/**/*.[jt]s?(x)"], | ||||
| 				testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"] | ||||
| 			}, | ||||
| 			{ | ||||
| 				displayName: "electron", | ||||
| 				testMatch: ["**/tests/electron/**/*.[jt]s?(x)"], | ||||
| 				testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"] | ||||
| 			}, | ||||
| 			{ | ||||
| 				displayName: "e2e", | ||||
| 				setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"], | ||||
| 				testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"], | ||||
| 				modulePaths: ["<rootDir>/js/"], | ||||
| 				testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"] | ||||
| 			} | ||||
| 		], | ||||
| 		collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"], | ||||
| 		coverageReporters: ["lcov", "text"], | ||||
| 		coverageProvider: "v8" | ||||
| 	}; | ||||
| }; | ||||
							
								
								
									
										164
									
								
								js/animateCSS.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								js/animateCSS.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| /* MagicMirror² | ||||
|  * AnimateCSS System from https://animate.style/ | ||||
|  * by @bugsounet | ||||
|  * for Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
|  | ||||
| /* enumeration of animations in Array **/ | ||||
| const AnimateCSSIn = [ | ||||
| 	// Attention seekers | ||||
| 	"bounce", | ||||
| 	"flash", | ||||
| 	"pulse", | ||||
| 	"rubberBand", | ||||
| 	"shakeX", | ||||
| 	"shakeY", | ||||
| 	"headShake", | ||||
| 	"swing", | ||||
| 	"tada", | ||||
| 	"wobble", | ||||
| 	"jello", | ||||
| 	"heartBeat", | ||||
| 	// Back entrances | ||||
| 	"backInDown", | ||||
| 	"backInLeft", | ||||
| 	"backInRight", | ||||
| 	"backInUp", | ||||
| 	// Bouncing entrances | ||||
| 	"bounceIn", | ||||
| 	"bounceInDown", | ||||
| 	"bounceInLeft", | ||||
| 	"bounceInRight", | ||||
| 	"bounceInUp", | ||||
| 	// Fading entrances | ||||
| 	"fadeIn", | ||||
| 	"fadeInDown", | ||||
| 	"fadeInDownBig", | ||||
| 	"fadeInLeft", | ||||
| 	"fadeInLeftBig", | ||||
| 	"fadeInRight", | ||||
| 	"fadeInRightBig", | ||||
| 	"fadeInUp", | ||||
| 	"fadeInUpBig", | ||||
| 	"fadeInTopLeft", | ||||
| 	"fadeInTopRight", | ||||
| 	"fadeInBottomLeft", | ||||
| 	"fadeInBottomRight", | ||||
| 	// Flippers | ||||
| 	"flip", | ||||
| 	"flipInX", | ||||
| 	"flipInY", | ||||
| 	// Lightspeed | ||||
| 	"lightSpeedInRight", | ||||
| 	"lightSpeedInLeft", | ||||
| 	// Rotating entrances | ||||
| 	"rotateIn", | ||||
| 	"rotateInDownLeft", | ||||
| 	"rotateInDownRight", | ||||
| 	"rotateInUpLeft", | ||||
| 	"rotateInUpRight", | ||||
| 	// Specials | ||||
| 	"jackInTheBox", | ||||
| 	"rollIn", | ||||
| 	// Zooming entrances | ||||
| 	"zoomIn", | ||||
| 	"zoomInDown", | ||||
| 	"zoomInLeft", | ||||
| 	"zoomInRight", | ||||
| 	"zoomInUp", | ||||
| 	// Sliding entrances | ||||
| 	"slideInDown", | ||||
| 	"slideInLeft", | ||||
| 	"slideInRight", | ||||
| 	"slideInUp" | ||||
| ]; | ||||
|  | ||||
| const AnimateCSSOut = [ | ||||
| 	// Back exits | ||||
| 	"backOutDown", | ||||
| 	"backOutLeft", | ||||
| 	"backOutRight", | ||||
| 	"backOutUp", | ||||
| 	// Bouncing exits | ||||
| 	"bounceOut", | ||||
| 	"bounceOutDown", | ||||
| 	"bounceOutLeft", | ||||
| 	"bounceOutRight", | ||||
| 	"bounceOutUp", | ||||
| 	// Fading exits | ||||
| 	"fadeOut", | ||||
| 	"fadeOutDown", | ||||
| 	"fadeOutDownBig", | ||||
| 	"fadeOutLeft", | ||||
| 	"fadeOutLeftBig", | ||||
| 	"fadeOutRight", | ||||
| 	"fadeOutRightBig", | ||||
| 	"fadeOutUp", | ||||
| 	"fadeOutUpBig", | ||||
| 	"fadeOutTopLeft", | ||||
| 	"fadeOutTopRight", | ||||
| 	"fadeOutBottomRight", | ||||
| 	"fadeOutBottomLeft", | ||||
| 	// Flippers | ||||
| 	"flipOutX", | ||||
| 	"flipOutY", | ||||
| 	// Lightspeed | ||||
| 	"lightSpeedOutRight", | ||||
| 	"lightSpeedOutLeft", | ||||
| 	// Rotating exits | ||||
| 	"rotateOut", | ||||
| 	"rotateOutDownLeft", | ||||
| 	"rotateOutDownRight", | ||||
| 	"rotateOutUpLeft", | ||||
| 	"rotateOutUpRight", | ||||
| 	// Specials | ||||
| 	"hinge", | ||||
| 	"rollOut", | ||||
| 	// Zooming exits | ||||
| 	"zoomOut", | ||||
| 	"zoomOutDown", | ||||
| 	"zoomOutLeft", | ||||
| 	"zoomOutRight", | ||||
| 	"zoomOutUp", | ||||
| 	// Sliding exits | ||||
| 	"slideOutDown", | ||||
| 	"slideOutLeft", | ||||
| 	"slideOutRight", | ||||
| 	"slideOutUp" | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|  * Create an animation with Animate CSS | ||||
|  * @param {string} [element] div element to animate. | ||||
|  * @param {string} [animation] animation name. | ||||
|  * @param {number} [animationTime] animation duration. | ||||
|  */ | ||||
| function addAnimateCSS(element, animation, animationTime) { | ||||
| 	const animationName = `animate__${animation}`; | ||||
| 	const node = document.getElementById(element); | ||||
| 	if (!node) { | ||||
| 		// don't execute animate: we don't find div | ||||
| 		Log.warn(`addAnimateCSS: node not found for`, element); | ||||
| 		return; | ||||
| 	} | ||||
| 	node.style.setProperty("--animate-duration", `${animationTime}s`); | ||||
| 	node.classList.add("animate__animated", animationName); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Remove an animation with Animate CSS | ||||
|  * @param {string} [element] div element to animate. | ||||
|  * @param {string} [animation] animation name. | ||||
|  */ | ||||
| function removeAnimateCSS(element, animation) { | ||||
| 	const animationName = `animate__${animation}`; | ||||
| 	const node = document.getElementById(element); | ||||
| 	if (!node) { | ||||
| 		// don't execute animate: we don't find div | ||||
| 		Log.warn(`removeAnimateCSS: node not found for`, element); | ||||
| 		return; | ||||
| 	} | ||||
| 	node.classList.remove("animate__animated", animationName); | ||||
| 	node.style.removeProperty("--animate-duration"); | ||||
| } | ||||
							
								
								
									
										323
									
								
								js/app.js
									
									
									
									
									
								
							
							
						
						
									
										323
									
								
								js/app.js
									
									
									
									
									
								
							| @@ -1,25 +1,27 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * The Core App (Server) | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var fs = require("fs"); | ||||
| var path = require("path"); | ||||
| var Log = require(__dirname + "/logger.js"); | ||||
| var Server = require(__dirname + "/server.js"); | ||||
| var Utils = require(__dirname + "/utils.js"); | ||||
| var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js"); | ||||
|  | ||||
| // Alias modules mentioned in package.js under _moduleAliases. | ||||
| require("module-alias/register"); | ||||
|  | ||||
| const fs = require("fs"); | ||||
| const path = require("path"); | ||||
| const envsub = require("envsub"); | ||||
| const Log = require("logger"); | ||||
| const Server = require(`${__dirname}/server`); | ||||
| const Utils = require(`${__dirname}/utils`); | ||||
| const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`); | ||||
|  | ||||
| // Get version number. | ||||
| global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version; | ||||
| Log.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,110 +38,154 @@ if (process.env.MM_PORT) { | ||||
| process.on("uncaughtException", function (err) { | ||||
| 	Log.error("Whoops! There was an uncaught exception..."); | ||||
| 	Log.error(err); | ||||
| 	Log.error("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?"); | ||||
| 	Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?"); | ||||
| 	Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues"); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * The core app. | ||||
|  * | ||||
|  * @class | ||||
|  */ | ||||
| var App = function () { | ||||
| 	var nodeHelpers = []; | ||||
| function App() { | ||||
| 	let nodeHelpers = []; | ||||
| 	let httpServer; | ||||
|  | ||||
| 	/** | ||||
| 	 * Loads the config file. Combines it with the defaults,  and runs the | ||||
| 	 * callback with the found config as argument. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function to be called after loading the config | ||||
| 	 * Loads the config file. Combines it with the defaults and returns the config | ||||
| 	 * @async | ||||
| 	 * @returns {Promise<object>} the loaded config or the defaults if something goes wrong | ||||
| 	 */ | ||||
| 	var loadConfig = function (callback) { | ||||
| 	async function loadConfig() { | ||||
| 		Log.log("Loading config ..."); | ||||
| 		var defaults = require(__dirname + "/defaults.js"); | ||||
| 		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`); | ||||
| 		let templateFile = `${configFilename}.template`; | ||||
|  | ||||
| 		// check if templateFile exists | ||||
| 		try { | ||||
| 			fs.accessSync(templateFile, fs.F_OK); | ||||
| 		} catch (err) { | ||||
| 			templateFile = null; | ||||
| 			Log.debug("config template file not exists, no envsubst"); | ||||
| 		} | ||||
|  | ||||
| 		if (templateFile) { | ||||
| 			// save current config.js | ||||
| 			try { | ||||
| 				if (fs.existsSync(configFilename)) { | ||||
| 					fs.copyFileSync(configFilename, `${configFilename}-old`); | ||||
| 				} | ||||
| 			} catch (err) { | ||||
| 				Log.warn(`Could not copy ${configFilename}: ${err.message}`); | ||||
| 			} | ||||
|  | ||||
| 			// check if config.env exists | ||||
| 			const envFiles = []; | ||||
| 			const configEnvFile = `${configFilename.substr(0, configFilename.lastIndexOf("."))}.env`; | ||||
| 			try { | ||||
| 				if (fs.existsSync(configEnvFile)) { | ||||
| 					envFiles.push(configEnvFile); | ||||
| 				} | ||||
| 			} catch (err) { | ||||
| 				Log.debug(`${configEnvFile} does not exist. ${err.message}`); | ||||
| 			} | ||||
|  | ||||
| 			let options = { | ||||
| 				all: true, | ||||
| 				diff: false, | ||||
| 				envFiles: envFiles, | ||||
| 				protect: false, | ||||
| 				syntax: "default", | ||||
| 				system: true | ||||
| 			}; | ||||
|  | ||||
| 			// envsubst variables in templateFile and create new config.js | ||||
| 			// naming for envsub must be templateFile and outputFile | ||||
| 			const outputFile = configFilename; | ||||
| 			try { | ||||
| 				await envsub({ templateFile, outputFile, options }); | ||||
| 			} catch (err) { | ||||
| 				Log.error(`Could not envsubst variables: ${err.message}`); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			fs.accessSync(configFilename, fs.F_OK); | ||||
| 			var c = require(configFilename); | ||||
| 			const c = require(configFilename); | ||||
| 			checkDeprecatedOptions(c); | ||||
| 			var config = Object.assign(defaults, c); | ||||
| 			callback(config); | ||||
| 			return Object.assign(defaults, c); | ||||
| 		} catch (e) { | ||||
| 			if (e.code === "ENOENT") { | ||||
| 				Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration.")); | ||||
| 			} else if (e instanceof ReferenceError || e instanceof SyntaxError) { | ||||
| 				Log.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack)); | ||||
| 				Log.error(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 { | ||||
| 				Log.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); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 		return defaults; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks the config for deprecated options and throws a warning in the logs | ||||
| 	 * if it encounters one option from the deprecated.js list | ||||
| 	 * | ||||
| 	 * @param {object} userConfig The user config | ||||
| 	 */ | ||||
| 	var checkDeprecatedOptions = function (userConfig) { | ||||
| 		var deprecated = require(global.root_path + "/js/deprecated.js"); | ||||
| 		var deprecatedOptions = deprecated.configs; | ||||
| 	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) { | ||||
| 			Log.warn(Utils.colors.warn("WARNING! Your config is using deprecated options: " + usedDeprecated.join(", ") + ". Check README and CHANGELOG for more up-to-date ways of getting the same functionality.")); | ||||
| 			Log.warn(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.`)); | ||||
| 		} | ||||
| 	}; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Loads a specific module. | ||||
| 	 * | ||||
| 	 * @param {string} module The name of the module (including subpath). | ||||
| 	 * @param {Function} callback Function to be called after loading | ||||
| 	 */ | ||||
| 	var loadModule = function (module, callback) { | ||||
| 		var elements = module.split("/"); | ||||
| 		var moduleName = elements[elements.length - 1]; | ||||
| 		var moduleFolder = __dirname + "/../modules/" + module; | ||||
| 	function loadModule(module) { | ||||
| 		const elements = module.split("/"); | ||||
| 		const moduleName = elements[elements.length - 1]; | ||||
| 		let 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 moduleFile = `${moduleFolder}/${module}.js`; | ||||
|  | ||||
| 		var loadModule = true; | ||||
| 		try { | ||||
| 			fs.accessSync(moduleFile, fs.R_OK); | ||||
| 		} catch (e) { | ||||
| 			Log.warn(`No ${moduleFile} found for module: ${moduleName}.`); | ||||
| 		} | ||||
|  | ||||
| 		const helperPath = `${moduleFolder}/node_helper.js`; | ||||
|  | ||||
| 		let loadHelper = true; | ||||
| 		try { | ||||
| 			fs.accessSync(helperPath, fs.R_OK); | ||||
| 		} catch (e) { | ||||
| 			loadModule = false; | ||||
| 			Log.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) { | ||||
| 				Log.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version:  " + m.requiresVersion + " - Current version: " + global.version); | ||||
| 				Log.log(`Check MagicMirror² version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`); | ||||
| 				if (cmpVersions(global.version, m.requiresVersion) >= 0) { | ||||
| 					Log.log("Version is ok!"); | ||||
| 				} else { | ||||
| 					Log.log("Version is incorrect. Skip module: '" + moduleName + "'"); | ||||
| 					Log.warn(`Version is incorrect. Skip module: '${moduleName}'`); | ||||
| 					return; | ||||
| 				} | ||||
| 			} | ||||
| @@ -148,53 +194,38 @@ var App = function () { | ||||
| 			m.setPath(path.resolve(moduleFolder)); | ||||
| 			nodeHelpers.push(m); | ||||
|  | ||||
| 			m.loaded(callback); | ||||
| 		} else { | ||||
| 			callback(); | ||||
| 			m.loaded(); | ||||
| 		} | ||||
| 	}; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Loads all modules. | ||||
| 	 * | ||||
| 	 * @param {Module[]} modules All modules to be loaded | ||||
| 	 * @param {Function} callback Function to be called after loading | ||||
| 	 * @returns {Promise} A promise that is resolved when all modules been loaded | ||||
| 	 */ | ||||
| 	var loadModules = function (modules, callback) { | ||||
| 	async function loadModules(modules) { | ||||
| 		Log.log("Loading module helpers ..."); | ||||
|  | ||||
| 		var loadNextModule = function () { | ||||
| 			if (modules.length > 0) { | ||||
| 				var nextModule = modules[0]; | ||||
| 				loadModule(nextModule, function () { | ||||
| 					modules = modules.slice(1); | ||||
| 					loadNextModule(); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				// All modules are loaded | ||||
| 				Log.log("All module helpers loaded."); | ||||
| 				callback(); | ||||
| 			} | ||||
| 		}; | ||||
| 		for (let module of modules) { | ||||
| 			await loadModule(module); | ||||
| 		} | ||||
|  | ||||
| 		loadNextModule(); | ||||
| 	}; | ||||
| 		Log.log("All module helpers loaded."); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Compare two semantic version numbers and return the difference. | ||||
| 	 * | ||||
| 	 * @param {string} a Version number a. | ||||
| 	 * @param {string} b Version number b. | ||||
| 	 * | ||||
| 	 * @returns {number} A positive number if a is larger than b, a negative | ||||
| 	 * number if a is smaller and 0 if they are the same | ||||
| 	 */ | ||||
| 	function cmpVersions(a, b) { | ||||
| 		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); | ||||
| @@ -208,45 +239,53 @@ var App = function () { | ||||
| 	/** | ||||
| 	 * Start the core app. | ||||
| 	 * | ||||
| 	 * It loads the config, then it loads all modules. When it's done it | ||||
| 	 * executes the callback with the config as argument. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function to be called after start | ||||
| 	 * It loads the config, then it loads all modules. | ||||
| 	 * @async | ||||
| 	 * @returns {Promise<object>} the config used | ||||
| 	 */ | ||||
| 	this.start = function (callback) { | ||||
| 		loadConfig(function (c) { | ||||
| 			config = c; | ||||
| 	this.start = async function () { | ||||
| 		config = await loadConfig(); | ||||
|  | ||||
| 			Log.setLogLevel(config.logLevel); | ||||
| 		Log.setLogLevel(config.logLevel); | ||||
|  | ||||
| 			var modules = []; | ||||
|  | ||||
| 			for (var m in config.modules) { | ||||
| 				var module = config.modules[m]; | ||||
| 				if (modules.indexOf(module.module) === -1 && !module.disabled) { | ||||
| 					modules.push(module.module); | ||||
| 				} | ||||
| 		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) { | ||||
| 					Log.log("Server started ..."); | ||||
| 		await loadModules(modules); | ||||
|  | ||||
| 					for (var h in nodeHelpers) { | ||||
| 						var nodeHelper = nodeHelpers[h]; | ||||
| 						nodeHelper.setExpressApp(app); | ||||
| 						nodeHelper.setSocketIO(io); | ||||
| 						nodeHelper.start(); | ||||
| 					} | ||||
| 		httpServer = new Server(config); | ||||
| 		const { app, io } = await httpServer.open(); | ||||
| 		Log.log("Server started ..."); | ||||
|  | ||||
| 					Log.log("Sockets connected & modules started ..."); | ||||
| 		const nodePromises = []; | ||||
| 		for (let nodeHelper of nodeHelpers) { | ||||
| 			nodeHelper.setExpressApp(app); | ||||
| 			nodeHelper.setSocketIO(io); | ||||
|  | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(config); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 			try { | ||||
| 				nodePromises.push(nodeHelper.start()); | ||||
| 			} catch (error) { | ||||
| 				Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`); | ||||
| 				Log.error(error); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		const results = await Promise.allSettled(nodePromises); | ||||
|  | ||||
| 		// Log errors that happened during async node_helper startup | ||||
| 		results.forEach((result) => { | ||||
| 			if (result.status === "rejected") { | ||||
| 				Log.error(result.reason); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		Log.log("Sockets connected & modules started ..."); | ||||
|  | ||||
| 		return config; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| @@ -254,14 +293,40 @@ var App = function () { | ||||
| 	 * exists. | ||||
| 	 * | ||||
| 	 * Added to fix #1056 | ||||
| 	 * @returns {Promise} A promise that is resolved when all node_helpers and | ||||
| 	 * the http server has been closed | ||||
| 	 */ | ||||
| 	this.stop = function () { | ||||
| 		for (var h in nodeHelpers) { | ||||
| 			var nodeHelper = nodeHelpers[h]; | ||||
| 			if (typeof nodeHelper.stop === "function") { | ||||
| 				nodeHelper.stop(); | ||||
| 	this.stop = async function () { | ||||
| 		const nodePromises = []; | ||||
| 		for (let nodeHelper of nodeHelpers) { | ||||
| 			try { | ||||
| 				if (typeof nodeHelper.stop === "function") { | ||||
| 					nodePromises.push(nodeHelper.stop()); | ||||
| 				} | ||||
| 			} catch (error) { | ||||
| 				Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`); | ||||
| 				console.error(error); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		const results = await Promise.allSettled(nodePromises); | ||||
|  | ||||
| 		// Log errors that happened during async node_helper stopping | ||||
| 		results.forEach((result) => { | ||||
| 			if (result.status === "rejected") { | ||||
| 				Log.error(result.reason); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		Log.log("Node_helpers stopped ..."); | ||||
|  | ||||
| 		// To be able to stop the app even if it hasn't been started (when | ||||
| 		// running with Electron against another server) | ||||
| 		if (!httpServer) { | ||||
| 			return Promise.resolve(); | ||||
| 		} | ||||
|  | ||||
| 		return httpServer.close(); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| @@ -271,12 +336,12 @@ var App = function () { | ||||
| 	 * Note: this is only used if running `server-only`. Otherwise | ||||
| 	 * this.stop() is called by app.on("before-quit"... in `electron.js` | ||||
| 	 */ | ||||
| 	process.on("SIGINT", () => { | ||||
| 	process.on("SIGINT", async () => { | ||||
| 		Log.log("[SIGINT] Received. Shutting down server..."); | ||||
| 		setTimeout(() => { | ||||
| 			process.exit(0); | ||||
| 		}, 3000); // Force quit after 3 seconds | ||||
| 		this.stop(); | ||||
| 		await this.stop(); | ||||
| 		process.exit(0); | ||||
| 	}); | ||||
|  | ||||
| @@ -284,14 +349,14 @@ var App = function () { | ||||
| 	 * Listen to SIGTERM signals so we can stop everything when we | ||||
| 	 * are asked to stop by the OS. | ||||
| 	 */ | ||||
| 	process.on("SIGTERM", () => { | ||||
| 	process.on("SIGTERM", async () => { | ||||
| 		Log.log("[SIGTERM] Received. Shutting down server..."); | ||||
| 		setTimeout(() => { | ||||
| 			process.exit(0); | ||||
| 		}, 3000); // Force quit after 3 seconds | ||||
| 		this.stop(); | ||||
| 		await this.stop(); | ||||
| 		process.exit(0); | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
|  | ||||
| module.exports = new App(); | ||||
|   | ||||
| @@ -1,33 +1,28 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * | ||||
|  * Check the configuration file for errors | ||||
|  * | ||||
|  * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const Linter = require("eslint").Linter; | ||||
| const linter = new Linter(); | ||||
|  | ||||
| const path = require("path"); | ||||
| const fs = require("fs"); | ||||
| const { Linter } = require("eslint"); | ||||
|  | ||||
| const rootPath = path.resolve(__dirname + "/../"); | ||||
| const Log = require(rootPath + "/js/logger.js"); | ||||
| const Utils = require(rootPath + "/js/utils.js"); | ||||
| const linter = new Linter(); | ||||
|  | ||||
| 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! | ||||
| 	let configFileName = path.resolve(rootPath + "/config/config.js"); | ||||
| 	if (process.env.MM_CONFIG_FILE) { | ||||
| 		configFileName = path.resolve(process.env.MM_CONFIG_FILE); | ||||
| 	} | ||||
| 	return configFileName; | ||||
| 	return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -54,21 +49,24 @@ function checkConfigFile() { | ||||
| 	Log.info(Utils.colors.info("Checking file... "), configFileName); | ||||
|  | ||||
| 	// I'm not sure if all ever is utf-8 | ||||
| 	fs.readFile(configFileName, "utf-8", function (err, data) { | ||||
| 		if (err) { | ||||
| 			throw err; | ||||
| 		} | ||||
| 		const messages = linter.verify(data); | ||||
| 		if (messages.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 :(")); | ||||
| 			// In case the there errors show messages and return | ||||
| 			messages.forEach((error) => { | ||||
| 				Log.error("Line", error.line, "col", error.column, error.message); | ||||
| 			}); | ||||
| 	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(); | ||||
|   | ||||
							
								
								
									
										22
									
								
								js/class.js
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								js/class.js
									
									
									
									
									
								
							| @@ -8,8 +8,8 @@ | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| (function () { | ||||
| 	var initializing = false; | ||||
| 	var fnTest = /xyz/.test(function () { | ||||
| 	let initializing = false; | ||||
| 	const fnTest = /xyz/.test(function () { | ||||
| 		xyz; | ||||
| 	}) | ||||
| 		? /\b_super\b/ | ||||
| @@ -20,27 +20,27 @@ | ||||
|  | ||||
| 	// Create a new Class that inherits from this class | ||||
| 	Class.extend = function (prop) { | ||||
| 		var _super = this.prototype; | ||||
| 		let _super = this.prototype; | ||||
|  | ||||
| 		// Instantiate a base class (but only create the instance, | ||||
| 		// don't run the init constructor) | ||||
| 		initializing = true; | ||||
| 		var prototype = new this(); | ||||
| 		const prototype = new this(); | ||||
| 		initializing = false; | ||||
|  | ||||
| 		// Make a copy of all prototype properties, to prevent reference issues. | ||||
| 		for (var p in prototype) { | ||||
| 		for (const p in prototype) { | ||||
| 			prototype[p] = cloneObject(prototype[p]); | ||||
| 		} | ||||
|  | ||||
| 		// Copy the properties over onto the new prototype | ||||
| 		for (var name in prop) { | ||||
| 		for (const name in prop) { | ||||
| 			// Check if we're overwriting an existing function | ||||
| 			prototype[name] = | ||||
| 				typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name]) | ||||
| 					? (function (name, fn) { | ||||
| 							return function () { | ||||
| 								var tmp = this._super; | ||||
| 								const tmp = this._super; | ||||
|  | ||||
| 								// Add a new ._super() method that is the same method | ||||
| 								// but on the super-class | ||||
| @@ -48,7 +48,7 @@ | ||||
|  | ||||
| 								// The method only need to be bound temporarily, so we | ||||
| 								// remove it when we're done executing | ||||
| 								var ret = fn.apply(this, arguments); | ||||
| 								const ret = fn.apply(this, arguments); | ||||
| 								this._super = tmp; | ||||
|  | ||||
| 								return ret; | ||||
| @@ -82,9 +82,7 @@ | ||||
|  | ||||
| /** | ||||
|  * Define the clone method for later use. Helper Method. | ||||
|  * | ||||
|  * @param {object} obj Object to be cloned | ||||
|  * | ||||
|  * @returns {object} the cloned object | ||||
|  */ | ||||
| function cloneObject(obj) { | ||||
| @@ -92,8 +90,8 @@ function cloneObject(obj) { | ||||
| 		return obj; | ||||
| 	} | ||||
|  | ||||
| 	var temp = obj.constructor(); // give temp the original obj's constructor | ||||
| 	for (var key in obj) { | ||||
| 	const temp = obj.constructor(); // give temp the original obj's constructor | ||||
| 	for (const key in obj) { | ||||
| 		temp[key] = cloneObject(obj[key]); | ||||
|  | ||||
| 		if (key === "lockStrings") { | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| /* global mmPort */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Config Defaults | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var address = "localhost"; | ||||
| var port = 8080; | ||||
| const address = "localhost"; | ||||
| let port = 8080; | ||||
| if (typeof mmPort !== "undefined") { | ||||
| 	port = mmPort; | ||||
| } | ||||
| var defaults = { | ||||
| const defaults = { | ||||
| 	address: address, | ||||
| 	port: port, | ||||
| 	basePath: "/", | ||||
| @@ -20,10 +20,19 @@ var defaults = { | ||||
| 	ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], | ||||
|  | ||||
| 	language: "en", | ||||
| 	logLevel: ["INFO", "LOG", "WARN", "ERROR"], | ||||
| 	timeFormat: 24, | ||||
| 	units: "metric", | ||||
| 	zoom: 1, | ||||
| 	customCss: "css/custom.css", | ||||
| 	// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js, | ||||
| 	// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MichMich/MagicMirror/issues/2847 | ||||
| 	httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false }, | ||||
|  | ||||
| 	// properties for checking if server is alive and has same startup-timestamp, the check is per default enabled | ||||
| 	// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage. | ||||
| 	checkServerInterval: 30 * 1000, | ||||
| 	reloadAfterServerRestart: false, | ||||
|  | ||||
| 	modules: [ | ||||
| 		{ | ||||
| @@ -35,14 +44,14 @@ var defaults = { | ||||
| 			position: "upper_third", | ||||
| 			classes: "large thin", | ||||
| 			config: { | ||||
| 				text: "Magic Mirror<sup>2</sup>" | ||||
| 				text: "MagicMirror²" | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| 			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 +67,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>" | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| /* Magic Mirror Deprecated Config Options List | ||||
| /* MagicMirror² Deprecated Config Options List | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
| @@ -6,11 +6,6 @@ | ||||
|  * Olex S. original idea this deprecated option | ||||
|  */ | ||||
|  | ||||
| var deprecated = { | ||||
| module.exports = { | ||||
| 	configs: ["kioskmode"] | ||||
| }; | ||||
|  | ||||
| /*************** DO NOT EDIT THE LINE BELOW ***************/ | ||||
| if (typeof module !== "undefined") { | ||||
| 	module.exports = deprecated; | ||||
| } | ||||
|   | ||||
							
								
								
									
										119
									
								
								js/electron.js
									
									
									
									
									
								
							
							
						
						
									
										119
									
								
								js/electron.js
									
									
									
									
									
								
							| @@ -1,13 +1,19 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const electron = require("electron"); | ||||
| const core = require("./app.js"); | ||||
| const Log = require("./logger.js"); | ||||
| const core = require("./app"); | ||||
| 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; | ||||
| // If ELECTRON_DISABLE_GPU is set electron is started with --disable-gpu flag. | ||||
| // See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info. | ||||
| if (process.env.ELECTRON_DISABLE_GPU !== undefined) { | ||||
| 	app.disableHardwareAcceleration(); | ||||
| } | ||||
|  | ||||
| // Module to create native browser window. | ||||
| const BrowserWindow = electron.BrowserWindow; | ||||
|  | ||||
| @@ -19,14 +25,25 @@ let mainWindow; | ||||
|  * | ||||
|  */ | ||||
| function createWindow() { | ||||
| 	app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); | ||||
| 	var electronOptionsDefaults = { | ||||
| 		width: 800, | ||||
| 		height: 600, | ||||
| 	// see https://www.electronjs.org/docs/latest/api/screen | ||||
| 	// Create a window that fills the screen's available work area. | ||||
| 	let electronSize = (800, 600); | ||||
| 	try { | ||||
| 		electronSize = electron.screen.getPrimaryDisplay().workAreaSize; | ||||
| 	} catch { | ||||
| 		Log.warn("Could not get display size, using defaults ..."); | ||||
| 	} | ||||
|  | ||||
| 	let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"]; | ||||
| 	app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches)); | ||||
| 	let electronOptionsDefaults = { | ||||
| 		width: electronSize.width, | ||||
| 		height: electronSize.height, | ||||
| 		x: 0, | ||||
| 		y: 0, | ||||
| 		darkTheme: true, | ||||
| 		webPreferences: { | ||||
| 			contextIsolation: true, | ||||
| 			nodeIntegration: false, | ||||
| 			zoomFactor: config.zoom | ||||
| 		}, | ||||
| @@ -38,11 +55,14 @@ function createWindow() { | ||||
| 	if (config.kioskmode) { | ||||
| 		electronOptionsDefaults.kiosk = true; | ||||
| 	} else { | ||||
| 		electronOptionsDefaults.show = false; | ||||
| 		electronOptionsDefaults.frame = false; | ||||
| 		electronOptionsDefaults.transparent = true; | ||||
| 		electronOptionsDefaults.hasShadow = false; | ||||
| 		electronOptionsDefaults.fullscreen = true; | ||||
| 		electronOptionsDefaults.autoHideMenuBar = true; | ||||
| 	} | ||||
|  | ||||
| 	var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions); | ||||
| 	const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions); | ||||
|  | ||||
| 	// Create the browser window. | ||||
| 	mainWindow = new BrowserWindow(electronOptions); | ||||
| @@ -50,21 +70,31 @@ function createWindow() { | ||||
| 	// 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 prefix; | ||||
| 	if (config["tls"] !== null && config["tls"]) { | ||||
| 	let prefix; | ||||
| 	if ((config["tls"] !== null && config["tls"]) || config.useHttps) { | ||||
| 		prefix = "https://"; | ||||
| 	} else { | ||||
| 		prefix = "http://"; | ||||
| 	} | ||||
|  | ||||
| 	var address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address; | ||||
| 	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 = null; | ||||
| @@ -85,18 +115,34 @@ function createWindow() { | ||||
| 			}, 1000); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // This method will be called when Electron has finished | ||||
| // initialization and is ready to create browser windows. | ||||
| app.on("ready", function () { | ||||
| 	Log.log("Launching application."); | ||||
| 	createWindow(); | ||||
| }); | ||||
| 	//remove response headers that prevent sites of being embedded into iframes if configured | ||||
| 	mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => { | ||||
| 		let curHeaders = details.responseHeaders; | ||||
| 		if (config["ignoreXOriginHeader"] || false) { | ||||
| 			curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/x-frame-options/i.test(header[0]))); | ||||
| 		} | ||||
|  | ||||
| 		if (config["ignoreContentSecurityPolicy"] || false) { | ||||
| 			curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/content-security-policy/i.test(header[0]))); | ||||
| 		} | ||||
|  | ||||
| 		callback({ responseHeaders: curHeaders }); | ||||
| 	}); | ||||
|  | ||||
| 	mainWindow.once("ready-to-show", () => { | ||||
| 		mainWindow.show(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| // Quit when all windows are closed. | ||||
| app.on("window-all-closed", function () { | ||||
| 	createWindow(); | ||||
| 	if (process.env.JEST_WORKER_ID !== undefined) { | ||||
| 		// if we are running with jest | ||||
| 		app.quit(); | ||||
| 	} else { | ||||
| 		createWindow(); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| app.on("activate", function () { | ||||
| @@ -113,20 +159,39 @@ app.on("activate", function () { | ||||
|  * Note: this is only used if running Electron. Otherwise | ||||
|  * core.stop() is called by process.on("SIGINT"... in `app.js` | ||||
|  */ | ||||
| app.on("before-quit", (event) => { | ||||
| app.on("before-quit", async (event) => { | ||||
| 	Log.log("Shutting down server..."); | ||||
| 	event.preventDefault(); | ||||
| 	setTimeout(() => { | ||||
| 		process.exit(0); | ||||
| 	}, 3000); // Force-quit after 3 seconds. | ||||
| 	core.stop(); | ||||
| 	await core.stop(); | ||||
| 	process.exit(0); | ||||
| }); | ||||
|  | ||||
| // 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) { | ||||
| 		config = c; | ||||
| /** | ||||
|  * Handle errors from self-signed certificates | ||||
|  */ | ||||
| app.on("certificate-error", (event, webContents, url, error, certificate, callback) => { | ||||
| 	event.preventDefault(); | ||||
| 	callback(true); | ||||
| }); | ||||
|  | ||||
| if (process.env.clientonly) { | ||||
| 	app.whenReady().then(() => { | ||||
| 		Log.log("Launching client viewer application."); | ||||
| 		createWindow(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| // Start the core application if server is run on localhost | ||||
| // This starts all node helpers and starts the webserver. | ||||
| if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) { | ||||
| 	core.start().then((c) => { | ||||
| 		config = c; | ||||
| 		app.whenReady().then(() => { | ||||
| 			Log.log("Launching application."); | ||||
| 			createWindow(); | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
							
								
								
									
										280
									
								
								js/loader.js
									
									
									
									
									
								
							
							
						
						
									
										280
									
								
								js/loader.js
									
									
									
									
									
								
							| @@ -1,216 +1,196 @@ | ||||
| /* global defaultModules, vendor */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Module and File loaders. | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var Loader = (function () { | ||||
| const Loader = (function () { | ||||
| 	/* Create helper variables */ | ||||
|  | ||||
| 	var loadedModuleFiles = []; | ||||
| 	var loadedFiles = []; | ||||
| 	var moduleObjects = []; | ||||
| 	const loadedModuleFiles = []; | ||||
| 	const loadedFiles = []; | ||||
| 	const moduleObjects = []; | ||||
|  | ||||
| 	/* Private Methods */ | ||||
|  | ||||
| 	/** | ||||
| 	 * Loops thru all modules and requests load for every module. | ||||
| 	 * Loops through all modules and requests start for every module. | ||||
| 	 */ | ||||
| 	var loadModules = function () { | ||||
| 		var moduleData = getModuleData(); | ||||
|  | ||||
| 		var loadNextModule = function () { | ||||
| 			if (moduleData.length > 0) { | ||||
| 				var nextModule = moduleData[0]; | ||||
| 				loadModule(nextModule, function () { | ||||
| 					moduleData = moduleData.slice(1); | ||||
| 					loadNextModule(); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				// All modules loaded. Load custom.css | ||||
| 				// This is done after all the modules so we can | ||||
| 				// overwrite all the defined styles. | ||||
|  | ||||
| 				loadFile(config.customCss, function () { | ||||
| 					// custom.css loaded. Start all modules. | ||||
| 					startModules(); | ||||
| 				}); | ||||
| 	const startModules = async function () { | ||||
| 		const modulePromises = []; | ||||
| 		for (const module of moduleObjects) { | ||||
| 			try { | ||||
| 				modulePromises.push(module.start()); | ||||
| 			} catch (error) { | ||||
| 				Log.error(`Error when starting node_helper for module ${module.name}:`); | ||||
| 				Log.error(error); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		loadNextModule(); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Loops thru all modules and requests start for every module. | ||||
| 	 */ | ||||
| 	var startModules = function () { | ||||
| 		for (var m in moduleObjects) { | ||||
| 			var module = moduleObjects[m]; | ||||
| 			module.start(); | ||||
| 		} | ||||
|  | ||||
| 		const results = await Promise.allSettled(modulePromises); | ||||
|  | ||||
| 		// Log errors that happened during async node_helper startup | ||||
| 		results.forEach((result) => { | ||||
| 			if (result.status === "rejected") { | ||||
| 				Log.error(result.reason); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		// Notify core of loaded modules. | ||||
| 		MM.modulesStarted(moduleObjects); | ||||
|  | ||||
| 		// Starting modules also hides any modules that have requested to be initially hidden | ||||
| 		for (const thisModule of moduleObjects) { | ||||
| 			if (thisModule.data.hiddenOnStartup) { | ||||
| 				Log.info(`Initially hiding ${thisModule.name}`); | ||||
| 				thisModule.hide(); | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Retrieve list of all modules. | ||||
| 	 * | ||||
| 	 * @returns {object[]} module data as configured in config | ||||
| 	 */ | ||||
| 	var getAllModules = function () { | ||||
| 	const getAllModules = function () { | ||||
| 		return config.modules; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Generate array with module information including module paths. | ||||
| 	 * | ||||
| 	 * @returns {object[]} Module information. | ||||
| 	 */ | ||||
| 	var getModuleData = function () { | ||||
| 		var modules = getAllModules(); | ||||
| 		var moduleFiles = []; | ||||
| 	const getModuleData = function () { | ||||
| 		const modules = getAllModules(); | ||||
| 		const moduleFiles = []; | ||||
|  | ||||
| 		for (var m in modules) { | ||||
| 			var moduleData = modules[m]; | ||||
| 			var module = moduleData.module; | ||||
| 		modules.forEach(function (moduleData, index) { | ||||
| 			const module = moduleData.module; | ||||
|  | ||||
| 			var elements = module.split("/"); | ||||
| 			var moduleName = elements[elements.length - 1]; | ||||
| 			var moduleFolder = config.paths.modules + "/" + module; | ||||
| 			const elements = module.split("/"); | ||||
| 			const moduleName = elements[elements.length - 1]; | ||||
| 			let moduleFolder = `${config.paths.modules}/${module}`; | ||||
|  | ||||
| 			if (defaultModules.indexOf(moduleName) !== -1) { | ||||
| 				moduleFolder = config.paths.modules + "/default/" + module; | ||||
| 				moduleFolder = `${config.paths.modules}/default/${module}`; | ||||
| 			} | ||||
|  | ||||
| 			if (moduleData.disabled === true) { | ||||
| 				continue; | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			moduleFiles.push({ | ||||
| 				index: m, | ||||
| 				identifier: "module_" + m + "_" + module, | ||||
| 				index: index, | ||||
| 				identifier: `module_${index}_${module}`, | ||||
| 				name: moduleName, | ||||
| 				path: moduleFolder + "/", | ||||
| 				file: moduleName + ".js", | ||||
| 				path: `${moduleFolder}/`, | ||||
| 				file: `${moduleName}.js`, | ||||
| 				position: moduleData.position, | ||||
| 				animateIn: moduleData.animateIn, | ||||
| 				animateOut: moduleData.animateOut, | ||||
| 				hiddenOnStartup: moduleData.hiddenOnStartup, | ||||
| 				header: moduleData.header, | ||||
| 				configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false, | ||||
| 				config: moduleData.config, | ||||
| 				classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module | ||||
| 				classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module | ||||
| 			}); | ||||
| 		} | ||||
| 		}); | ||||
|  | ||||
| 		return moduleFiles; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Load modules via ajax request and create module objects.s | ||||
| 	 * | ||||
| 	 * Load modules via ajax request and create module objects. | ||||
| 	 * @param {object} module Information about the module we want to load. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} resolved when module is loaded | ||||
| 	 */ | ||||
| 	var loadModule = function (module, callback) { | ||||
| 		var url = module.path + "/" + module.file; | ||||
| 	const loadModule = async function (module) { | ||||
| 		const url = module.path + module.file; | ||||
|  | ||||
| 		var afterLoad = function () { | ||||
| 			var moduleObject = Module.create(module.name); | ||||
| 		/** | ||||
| 		 * @returns {Promise<void>} | ||||
| 		 */ | ||||
| 		const afterLoad = async function () { | ||||
| 			const moduleObject = Module.create(module.name); | ||||
| 			if (moduleObject) { | ||||
| 				bootstrapModule(module, moduleObject, function () { | ||||
| 					callback(); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				callback(); | ||||
| 				await bootstrapModule(module, moduleObject); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		if (loadedModuleFiles.indexOf(url) !== -1) { | ||||
| 			afterLoad(); | ||||
| 			await afterLoad(); | ||||
| 		} else { | ||||
| 			loadFile(url, function () { | ||||
| 				loadedModuleFiles.push(url); | ||||
| 				afterLoad(); | ||||
| 			}); | ||||
| 			await loadFile(url); | ||||
| 			loadedModuleFiles.push(url); | ||||
| 			await afterLoad(); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Bootstrap modules by setting the module data and loading the scripts & styles. | ||||
| 	 * | ||||
| 	 * @param {object} module Information about the module we want to load. | ||||
| 	 * @param {Module} mObj Modules instance. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 */ | ||||
| 	var bootstrapModule = function (module, mObj, callback) { | ||||
| 		Log.info("Bootstrapping module: " + module.name); | ||||
|  | ||||
| 	const bootstrapModule = async function (module, mObj) { | ||||
| 		Log.info(`Bootstrapping module: ${module.name}`); | ||||
| 		mObj.setData(module); | ||||
|  | ||||
| 		mObj.loadScripts(function () { | ||||
| 			Log.log("Scripts loaded for: " + module.name); | ||||
| 			mObj.loadStyles(function () { | ||||
| 				Log.log("Styles loaded for: " + module.name); | ||||
| 				mObj.loadTranslations(function () { | ||||
| 					Log.log("Translations loaded for: " + module.name); | ||||
| 					moduleObjects.push(mObj); | ||||
| 					callback(); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 		await mObj.loadScripts(); | ||||
| 		Log.log(`Scripts loaded for: ${module.name}`); | ||||
|  | ||||
| 		await mObj.loadStyles(); | ||||
| 		Log.log(`Styles loaded for: ${module.name}`); | ||||
|  | ||||
| 		await mObj.loadTranslations(); | ||||
| 		Log.log(`Translations loaded for: ${module.name}`); | ||||
|  | ||||
| 		moduleObjects.push(mObj); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Load a script or stylesheet by adding it to the dom. | ||||
| 	 * | ||||
| 	 * @param {string} fileName Path of the file we want to load. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise} resolved when the file is loaded | ||||
| 	 */ | ||||
| 	var loadFile = function (fileName, callback) { | ||||
| 		var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1); | ||||
| 	const loadFile = async function (fileName) { | ||||
| 		const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1); | ||||
| 		let script, stylesheet; | ||||
|  | ||||
| 		switch (extension.toLowerCase()) { | ||||
| 			case "js": | ||||
| 				Log.log("Load script: " + fileName); | ||||
| 				var script = document.createElement("script"); | ||||
| 				script.type = "text/javascript"; | ||||
| 				script.src = fileName; | ||||
| 				script.onload = function () { | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}; | ||||
| 				script.onerror = function () { | ||||
| 					Log.error("Error on loading script:", fileName); | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}; | ||||
|  | ||||
| 				document.getElementsByTagName("body")[0].appendChild(script); | ||||
| 				break; | ||||
| 				return new Promise((resolve) => { | ||||
| 					Log.log(`Load script: ${fileName}`); | ||||
| 					script = document.createElement("script"); | ||||
| 					script.type = "text/javascript"; | ||||
| 					script.src = fileName; | ||||
| 					script.onload = function () { | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					script.onerror = function () { | ||||
| 						Log.error("Error on loading script:", fileName); | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					document.getElementsByTagName("body")[0].appendChild(script); | ||||
| 				}); | ||||
| 			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 () { | ||||
| 					Log.error("Error on loading stylesheet:", fileName); | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}; | ||||
| 				return new Promise((resolve) => { | ||||
| 					Log.log(`Load stylesheet: ${fileName}`); | ||||
|  | ||||
| 				document.getElementsByTagName("head")[0].appendChild(stylesheet); | ||||
| 				break; | ||||
| 					stylesheet = document.createElement("link"); | ||||
| 					stylesheet.rel = "stylesheet"; | ||||
| 					stylesheet.type = "text/css"; | ||||
| 					stylesheet.href = fileName; | ||||
| 					stylesheet.onload = function () { | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					stylesheet.onerror = function () { | ||||
| 						Log.error("Error on loading stylesheet:", fileName); | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					document.getElementsByTagName("head")[0].appendChild(stylesheet); | ||||
| 				}); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| @@ -219,22 +199,40 @@ var Loader = (function () { | ||||
| 		/** | ||||
| 		 * Load all modules as defined in the config. | ||||
| 		 */ | ||||
| 		loadModules: function () { | ||||
| 			loadModules(); | ||||
| 		loadModules: async function () { | ||||
| 			let moduleData = getModuleData(); | ||||
|  | ||||
| 			/** | ||||
| 			 * @returns {Promise<void>} when all modules are loaded | ||||
| 			 */ | ||||
| 			const loadNextModule = async function () { | ||||
| 				if (moduleData.length > 0) { | ||||
| 					const nextModule = moduleData[0]; | ||||
| 					await loadModule(nextModule); | ||||
| 					moduleData = moduleData.slice(1); | ||||
| 					await loadNextModule(); | ||||
| 				} else { | ||||
| 					// All modules loaded. Load custom.css | ||||
| 					// This is done after all the modules so we can | ||||
| 					// overwrite all the defined styles. | ||||
| 					await loadFile(config.customCss); | ||||
| 					// custom.css loaded. Start all modules. | ||||
| 					await startModules(); | ||||
| 				} | ||||
| 			}; | ||||
| 			await loadNextModule(); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Load a file (script or stylesheet). | ||||
| 		 * Prevent double loading and search for files in the vendor folder. | ||||
| 		 * | ||||
| 		 * @param {string} fileName Path of the file we want to load. | ||||
| 		 * @param {Module} module The module that calls the loadFile function. | ||||
| 		 * @param {Function} callback Function called when done. | ||||
| 		 * @returns {Promise} resolved when the file is loaded | ||||
| 		 */ | ||||
| 		loadFile: function (fileName, module, callback) { | ||||
| 		loadFileForModule: async function (fileName, module) { | ||||
| 			if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) { | ||||
| 				Log.log("File already loaded: " + fileName); | ||||
| 				callback(); | ||||
| 				Log.log(`File already loaded: ${fileName}`); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| @@ -242,22 +240,20 @@ var Loader = (function () { | ||||
| 				// This is an absolute or relative path. | ||||
| 				// Load it and then return. | ||||
| 				loadedFiles.push(fileName.toLowerCase()); | ||||
| 				loadFile(fileName, callback); | ||||
| 				return; | ||||
| 				return loadFile(fileName); | ||||
| 			} | ||||
|  | ||||
| 			if (vendor[fileName] !== undefined) { | ||||
| 				// This file is available in the vendor folder. | ||||
| 				// Load it from this vendor folder. | ||||
| 				loadedFiles.push(fileName.toLowerCase()); | ||||
| 				loadFile(config.paths.vendor + "/" + vendor[fileName], callback); | ||||
| 				return; | ||||
| 				return loadFile(`${config.paths.vendor}/${vendor[fileName]}`); | ||||
| 			} | ||||
|  | ||||
| 			// File not loaded yet. | ||||
| 			// Load it based on the module path. | ||||
| 			loadedFiles.push(fileName.toLowerCase()); | ||||
| 			loadFile(module.file(fileName), callback); | ||||
| 			return loadFile(module.file(fileName)); | ||||
| 		} | ||||
| 	}; | ||||
| })(); | ||||
|   | ||||
							
								
								
									
										87
									
								
								js/logger.js
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								js/logger.js
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Log | ||||
|  * | ||||
|  * This logger is very simple, but needs to be extended. | ||||
| @@ -9,12 +9,13 @@ | ||||
|  */ | ||||
| (function (root, factory) { | ||||
| 	if (typeof exports === "object") { | ||||
| 		// 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"] | ||||
| 		}); | ||||
|  | ||||
| 		if (process.env.JEST_WORKER_ID === undefined) { | ||||
| 			// add timestamps in front of log messages | ||||
| 			require("console-stamp")(console, { | ||||
| 				pattern: "yyyy-mm-dd HH:MM:ss.l", | ||||
| 				include: ["debug", "log", "info", "warn", "error"] | ||||
| 			}); | ||||
| 		} | ||||
| 		// Node, CommonJS-like | ||||
| 		module.exports = factory(root.config); | ||||
| 	} else { | ||||
| @@ -22,29 +23,57 @@ | ||||
| 		root.Log = factory(root.config); | ||||
| 	} | ||||
| })(this, function (config) { | ||||
| 	const 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) | ||||
| 	}; | ||||
| 	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"; | ||||
| 	} | ||||
|  | ||||
| 	logLevel.setLogLevel = function (newLevel) { | ||||
| 		if (newLevel) { | ||||
| 			Object.keys(logLevel).forEach(function (key, index) { | ||||
| 				if (!newLevel.includes(key.toLocaleUpperCase())) { | ||||
| 					logLevel[key] = function () {}; | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	}; | ||||
| 	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) | ||||
| 		}; | ||||
|  | ||||
| 		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; | ||||
| }); | ||||
|   | ||||
							
								
								
									
										382
									
								
								js/main.js
									
									
									
									
									
								
							
							
						
						
									
										382
									
								
								js/main.js
									
									
									
									
									
								
							| @@ -1,41 +1,45 @@ | ||||
| /* global Loader, defaults, Translator */ | ||||
| /* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Main System | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var MM = (function () { | ||||
| 	var modules = []; | ||||
| const MM = (function () { | ||||
| 	let modules = []; | ||||
|  | ||||
| 	/* Private Methods */ | ||||
|  | ||||
| 	/** | ||||
| 	 * Create dom objects for all modules that are configured for a specific position. | ||||
| 	 */ | ||||
| 	var createDomObjects = function () { | ||||
| 		var domCreationPromises = []; | ||||
| 	const createDomObjects = function () { | ||||
| 		const domCreationPromises = []; | ||||
|  | ||||
| 		modules.forEach(function (module) { | ||||
| 			if (typeof module.data.position !== "string") { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			var wrapper = selectWrapper(module.data.position); | ||||
| 			let haveAnimateIn = null; | ||||
| 			// check if have valid animateIn in module definition (module.data.animateIn) | ||||
| 			if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn; | ||||
|  | ||||
| 			var dom = document.createElement("div"); | ||||
| 			const wrapper = selectWrapper(module.data.position); | ||||
|  | ||||
| 			const dom = document.createElement("div"); | ||||
| 			dom.id = module.identifier; | ||||
| 			dom.className = module.name; | ||||
|  | ||||
| 			if (typeof module.data.classes === "string") { | ||||
| 				dom.className = "module " + dom.className + " " + module.data.classes; | ||||
| 				dom.className = `module ${dom.className} ${module.data.classes}`; | ||||
| 			} | ||||
|  | ||||
| 			dom.opacity = 0; | ||||
| 			wrapper.appendChild(dom); | ||||
|  | ||||
| 			var moduleHeader = document.createElement("header"); | ||||
| 			const moduleHeader = document.createElement("header"); | ||||
| 			moduleHeader.innerHTML = module.getHeader(); | ||||
| 			moduleHeader.className = "module-header"; | ||||
| 			dom.appendChild(moduleHeader); | ||||
| @@ -46,11 +50,16 @@ var MM = (function () { | ||||
| 				moduleHeader.style.display = "block;"; | ||||
| 			} | ||||
|  | ||||
| 			var moduleContent = document.createElement("div"); | ||||
| 			const moduleContent = document.createElement("div"); | ||||
| 			moduleContent.className = "module-content"; | ||||
| 			dom.appendChild(moduleContent); | ||||
|  | ||||
| 			var domCreationPromise = updateDom(module, 0); | ||||
| 			// create the domCreationPromise with AnimateCSS (with animateIn of module definition) | ||||
| 			// or just display it | ||||
| 			var domCreationPromise; | ||||
| 			if (haveAnimateIn) domCreationPromise = updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true); | ||||
| 			else domCreationPromise = updateDom(module, 0); | ||||
|  | ||||
| 			domCreationPromises.push(domCreationPromise); | ||||
| 			domCreationPromise | ||||
| 				.then(function () { | ||||
| @@ -68,16 +77,14 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Select the wrapper dom object for a specific position. | ||||
| 	 * | ||||
| 	 * @param {string} position The name of the position. | ||||
| 	 * | ||||
| 	 * @returns {HTMLElement} the wrapper element | ||||
| 	 * @returns {HTMLElement | void} the wrapper element | ||||
| 	 */ | ||||
| 	var selectWrapper = function (position) { | ||||
| 		var classes = position.replace("_", " "); | ||||
| 		var parentWrapper = document.getElementsByClassName(classes); | ||||
| 	const selectWrapper = function (position) { | ||||
| 		const classes = position.replace("_", " "); | ||||
| 		const parentWrapper = document.getElementsByClassName(classes); | ||||
| 		if (parentWrapper.length > 0) { | ||||
| 			var wrapper = parentWrapper[0].getElementsByClassName("container"); | ||||
| 			const wrapper = parentWrapper[0].getElementsByClassName("container"); | ||||
| 			if (wrapper.length > 0) { | ||||
| 				return wrapper[0]; | ||||
| 			} | ||||
| @@ -86,15 +93,14 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Send a notification to all modules. | ||||
| 	 * | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 * @param {Module} sender The module that sent the notification. | ||||
| 	 * @param {Module} [sendTo] The (optional) module to send the notification to. | ||||
| 	 */ | ||||
| 	var sendNotification = function (notification, payload, sender, sendTo) { | ||||
| 		for (var m in modules) { | ||||
| 			var module = modules[m]; | ||||
| 	const sendNotification = function (notification, payload, sender, sendTo) { | ||||
| 		for (const m in modules) { | ||||
| 			const module = modules[m]; | ||||
| 			if (module !== sender && (!sendTo || module === sendTo)) { | ||||
| 				module.notificationReceived(notification, payload, sender); | ||||
| 			} | ||||
| @@ -103,16 +109,33 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Update the dom for a specific module. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module that needs an update. | ||||
| 	 * @param {number} [speed] The (optional) number of microseconds for the animation. | ||||
| 	 * | ||||
| 	 * @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates) | ||||
| 	 * @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror) | ||||
| 	 * @returns {Promise} Resolved when the dom is fully updated. | ||||
| 	 */ | ||||
| 	var updateDom = function (module, speed) { | ||||
| 	const updateDom = function (module, updateOptions, createAnimatedDom = false) { | ||||
| 		return new Promise(function (resolve) { | ||||
| 			var newContentPromise = module.getDom(); | ||||
| 			var newHeader = module.getHeader(); | ||||
| 			let speed = updateOptions; | ||||
| 			let animateOut = null; | ||||
| 			let animateIn = null; | ||||
| 			if (typeof updateOptions === "object") { | ||||
| 				if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) { | ||||
| 					speed = updateOptions.options.speed; | ||||
| 					Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`); | ||||
| 					if (typeof updateOptions.options.animate === "object") { | ||||
| 						animateOut = updateOptions.options.animate.out; | ||||
| 						animateIn = updateOptions.options.animate.in; | ||||
| 						Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`); | ||||
| 					} | ||||
| 				} else { | ||||
| 					Log.debug(`updateDom: ${module.identifier} Has no speed in object`); | ||||
| 					speed = 0; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			const newHeader = module.getHeader(); | ||||
| 			let newContentPromise = module.getDom(); | ||||
|  | ||||
| 			if (!(newContentPromise instanceof Promise)) { | ||||
| 				// convert to a promise if not already one to avoid if/else's everywhere | ||||
| @@ -121,7 +144,7 @@ var MM = (function () { | ||||
|  | ||||
| 			newContentPromise | ||||
| 				.then(function (newContent) { | ||||
| 					var updatePromise = updateDomWithContent(module, speed, newHeader, newContent); | ||||
| 					const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom); | ||||
|  | ||||
| 					updatePromise.then(resolve).catch(Log.error); | ||||
| 				}) | ||||
| @@ -131,15 +154,16 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Update the dom with the specified content | ||||
| 	 * | ||||
| 	 * @param {Module} module The module that needs an update. | ||||
| 	 * @param {number} [speed] The (optional) number of microseconds for the animation. | ||||
| 	 * @param {string} newHeader The new header that is generated. | ||||
| 	 * @param {HTMLElement} newContent The new content that is generated. | ||||
| 	 * | ||||
| 	 * @param {string} [animateOut] AnimateCss animation name before hidden | ||||
| 	 * @param {string} [animateIn] AnimateCss animation name on show | ||||
| 	 * @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start) | ||||
| 	 * @returns {Promise} Resolved when the module dom has been updated. | ||||
| 	 */ | ||||
| 	var updateDomWithContent = function (module, speed, newHeader, newContent) { | ||||
| 	const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) { | ||||
| 		return new Promise(function (resolve) { | ||||
| 			if (module.hidden || !speed) { | ||||
| 				updateModuleContent(module, newHeader, newContent); | ||||
| @@ -158,42 +182,55 @@ var MM = (function () { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			hideModule(module, speed / 2, function () { | ||||
| 			if (createAnimatedDom && animateIn !== null) { | ||||
| 				Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`); | ||||
| 				updateModuleContent(module, newHeader, newContent); | ||||
| 				if (!module.hidden) { | ||||
| 					showModule(module, speed / 2); | ||||
| 					showModule(module, speed, null, { animate: animateIn }); | ||||
| 				} | ||||
| 				resolve(); | ||||
| 			}); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			hideModule( | ||||
| 				module, | ||||
| 				speed / 2, | ||||
| 				function () { | ||||
| 					updateModuleContent(module, newHeader, newContent); | ||||
| 					if (!module.hidden) { | ||||
| 						showModule(module, speed / 2, null, { animate: animateIn }); | ||||
| 					} | ||||
| 					resolve(); | ||||
| 				}, | ||||
| 				{ animate: animateOut } | ||||
| 			); | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Check if the content has changed. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to check. | ||||
| 	 * @param {string} newHeader The new header that is generated. | ||||
| 	 * @param {HTMLElement} newContent The new content that is generated. | ||||
| 	 * | ||||
| 	 * @returns {boolean} True if the module need an update, false otherwise | ||||
| 	 */ | ||||
| 	var moduleNeedsUpdate = function (module, newHeader, newContent) { | ||||
| 		var moduleWrapper = document.getElementById(module.identifier); | ||||
| 	const moduleNeedsUpdate = function (module, newHeader, newContent) { | ||||
| 		const moduleWrapper = document.getElementById(module.identifier); | ||||
| 		if (moduleWrapper === null) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		var contentWrapper = moduleWrapper.getElementsByClassName("module-content"); | ||||
| 		var headerWrapper = moduleWrapper.getElementsByClassName("module-header"); | ||||
| 		const contentWrapper = moduleWrapper.getElementsByClassName("module-content"); | ||||
| 		const headerWrapper = moduleWrapper.getElementsByClassName("module-header"); | ||||
|  | ||||
| 		var headerNeedsUpdate = false; | ||||
| 		var contentNeedsUpdate = false; | ||||
| 		let headerNeedsUpdate = false; | ||||
| 		let contentNeedsUpdate; | ||||
|  | ||||
| 		if (headerWrapper.length > 0) { | ||||
| 			headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML; | ||||
| 		} | ||||
|  | ||||
| 		var tempContentWrapper = document.createElement("div"); | ||||
| 		const tempContentWrapper = document.createElement("div"); | ||||
| 		tempContentWrapper.appendChild(newContent); | ||||
| 		contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML; | ||||
|  | ||||
| @@ -202,18 +239,17 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Update the content of a module on screen. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to check. | ||||
| 	 * @param {string} newHeader The new header that is generated. | ||||
| 	 * @param {HTMLElement} newContent The new content that is generated. | ||||
| 	 */ | ||||
| 	var updateModuleContent = function (module, newHeader, newContent) { | ||||
| 		var moduleWrapper = document.getElementById(module.identifier); | ||||
| 	const updateModuleContent = function (module, newHeader, newContent) { | ||||
| 		const moduleWrapper = document.getElementById(module.identifier); | ||||
| 		if (moduleWrapper === null) { | ||||
| 			return; | ||||
| 		} | ||||
| 		var headerWrapper = moduleWrapper.getElementsByClassName("module-header"); | ||||
| 		var contentWrapper = moduleWrapper.getElementsByClassName("module-content"); | ||||
| 		const headerWrapper = moduleWrapper.getElementsByClassName("module-header"); | ||||
| 		const contentWrapper = moduleWrapper.getElementsByClassName("module-content"); | ||||
|  | ||||
| 		contentWrapper[0].innerHTML = ""; | ||||
| 		contentWrapper[0].appendChild(newContent); | ||||
| @@ -228,15 +264,12 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Hide the module. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to hide. | ||||
| 	 * @param {number} speed The speed of the hide animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the hide method. | ||||
| 	 */ | ||||
| 	var hideModule = function (module, speed, callback, options) { | ||||
| 		options = options || {}; | ||||
|  | ||||
| 	const hideModule = function (module, speed, callback, options = {}) { | ||||
| 		// set lockString if set in options. | ||||
| 		if (options.lockString) { | ||||
| 			// Log.log("Has lockstring: " + options.lockString); | ||||
| @@ -245,25 +278,67 @@ 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 () { | ||||
| 				// To not take up any space, we just make the position absolute. | ||||
| 				// since it's fade out anyway, we can see it lay above or | ||||
| 				// below other modules. This works way better than adjusting | ||||
| 				// the .display property. | ||||
| 				moduleWrapper.style.position = "fixed"; | ||||
| 			// reset all animations if needed | ||||
| 			if (module.hasAnimateOut) { | ||||
| 				removeAnimateCSS(module.identifier, module.hasAnimateOut); | ||||
| 				Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`); | ||||
| 				module.hasAnimateOut = false; | ||||
| 			} | ||||
| 			if (module.hasAnimateIn) { | ||||
| 				removeAnimateCSS(module.identifier, module.hasAnimateIn); | ||||
| 				Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`); | ||||
| 				module.hasAnimateIn = false; | ||||
| 			} | ||||
| 			// haveAnimateName for verify if we are using AninateCSS library | ||||
| 			// we check AnimateCSSOut Array for validate it | ||||
| 			// and finaly return the animate name or `null` (for default MM² animation) | ||||
| 			let haveAnimateName = null; | ||||
| 			// check if have valid animateOut in module definition (module.data.animateOut) | ||||
| 			if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut; | ||||
| 			// can't be override with options.animate | ||||
| 			else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate; | ||||
|  | ||||
| 				updateWrapperStates(); | ||||
| 			if (haveAnimateName) { | ||||
| 				// with AnimateCSS | ||||
| 				Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`); | ||||
| 				module.hasAnimateOut = haveAnimateName; | ||||
| 				addAnimateCSS(module.identifier, haveAnimateName, speed / 1000); | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					removeAnimateCSS(module.identifier, haveAnimateName); | ||||
| 					Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`); | ||||
| 					// AnimateCSS is now done | ||||
| 					moduleWrapper.style.opacity = 0; | ||||
| 					moduleWrapper.classList.add("hidden"); | ||||
| 					moduleWrapper.style.position = "fixed"; | ||||
| 					module.hasAnimateOut = false; | ||||
|  | ||||
| 				if (typeof callback === "function") { | ||||
| 					callback(); | ||||
| 				} | ||||
| 			}, speed); | ||||
| 					updateWrapperStates(); | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} else { | ||||
| 				// default MM² Animate | ||||
| 				moduleWrapper.style.transition = `opacity ${speed / 1000}s`; | ||||
| 				moduleWrapper.style.opacity = 0; | ||||
| 				moduleWrapper.classList.add("hidden"); | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					// To not take up any space, we just make the position absolute. | ||||
| 					// since it's fade out anyway, we can see it lay above or | ||||
| 					// below other modules. This works way better than adjusting | ||||
| 					// the .display property. | ||||
| 					moduleWrapper.style.position = "fixed"; | ||||
|  | ||||
| 					updateWrapperStates(); | ||||
|  | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} | ||||
| 		} else { | ||||
| 			// invoke callback even if no content, issue 1308 | ||||
| 			if (typeof callback === "function") { | ||||
| @@ -274,18 +349,15 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Show the module. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to show. | ||||
| 	 * @param {number} speed The speed of the show animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the show method. | ||||
| 	 */ | ||||
| 	var showModule = function (module, speed, callback, options) { | ||||
| 		options = options || {}; | ||||
|  | ||||
| 	const showModule = function (module, speed, callback, options = {}) { | ||||
| 		// remove lockString if set in options. | ||||
| 		if (options.lockString) { | ||||
| 			var index = module.lockStrings.indexOf(options.lockString); | ||||
| 			const index = module.lockStrings.indexOf(options.lockString); | ||||
| 			if (index !== -1) { | ||||
| 				module.lockStrings.splice(index, 1); | ||||
| 			} | ||||
| @@ -294,36 +366,77 @@ var MM = (function () { | ||||
| 		// Check if there are no more lockstrings set, or the force option is set. | ||||
| 		// Otherwise cancel show action. | ||||
| 		if (module.lockStrings.length !== 0 && options.force !== true) { | ||||
| 			Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(",")); | ||||
| 			Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`); | ||||
| 			if (typeof options.onError === "function") { | ||||
| 				options.onError(new Error("LOCK_STRING_ACTIVE")); | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
| 		// reset all animations if needed | ||||
| 		if (module.hasAnimateOut) { | ||||
| 			removeAnimateCSS(module.identifier, module.hasAnimateOut); | ||||
| 			Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`); | ||||
| 			module.hasAnimateOut = false; | ||||
| 		} | ||||
| 		if (module.hasAnimateIn) { | ||||
| 			removeAnimateCSS(module.identifier, module.hasAnimateIn); | ||||
| 			Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`); | ||||
| 			module.hasAnimateIn = false; | ||||
| 		} | ||||
|  | ||||
| 		module.hidden = false; | ||||
|  | ||||
| 		// If forced show, clean current lockstrings. | ||||
| 		if (module.lockStrings.length !== 0 && options.force === true) { | ||||
| 			Log.log("Force show of module: " + module.name); | ||||
| 			Log.log(`Force show of module: ${module.name}`); | ||||
| 			module.lockStrings = []; | ||||
| 		} | ||||
|  | ||||
| 		var moduleWrapper = document.getElementById(module.identifier); | ||||
| 		const moduleWrapper = document.getElementById(module.identifier); | ||||
| 		if (moduleWrapper !== null) { | ||||
| 			moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; | ||||
| 			clearTimeout(module.showHideTimer); | ||||
|  | ||||
| 			// haveAnimateName for verify if we are using AninateCSS library | ||||
| 			// we check AnimateCSSIn Array for validate it | ||||
| 			// and finaly return the animate name or `null` (for default MM² animation) | ||||
| 			let haveAnimateName = null; | ||||
| 			// check if have valid animateOut in module definition (module.data.animateIn) | ||||
| 			if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn; | ||||
| 			// can't be override with options.animate | ||||
| 			else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate; | ||||
|  | ||||
| 			if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`; | ||||
| 			// Restore the position. See hideModule() for more info. | ||||
| 			moduleWrapper.style.position = "static"; | ||||
| 			moduleWrapper.classList.remove("hidden"); | ||||
|  | ||||
| 			updateWrapperStates(); | ||||
|  | ||||
| 			// Waiting for DOM-changes done in updateWrapperStates before we can start the animation. | ||||
| 			var dummy = moduleWrapper.parentElement.parentElement.offsetHeight; | ||||
| 			const dummy = moduleWrapper.parentElement.parentElement.offsetHeight; | ||||
| 			moduleWrapper.style.opacity = 1; | ||||
|  | ||||
| 			clearTimeout(module.showHideTimer); | ||||
| 			module.showHideTimer = setTimeout(function () { | ||||
| 				if (typeof callback === "function") { | ||||
| 					callback(); | ||||
| 				} | ||||
| 			}, speed); | ||||
| 			if (haveAnimateName) { | ||||
| 				// with AnimateCSS | ||||
| 				Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`); | ||||
| 				module.hasAnimateIn = haveAnimateName; | ||||
| 				addAnimateCSS(module.identifier, haveAnimateName, speed / 1000); | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					removeAnimateCSS(module.identifier, haveAnimateName); | ||||
| 					Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`); | ||||
| 					module.hasAnimateIn = false; | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} else { | ||||
| 				// default MM² Animate | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} | ||||
| 		} else { | ||||
| 			// invoke callback | ||||
| 			if (typeof callback === "function") { | ||||
| @@ -343,14 +456,14 @@ var MM = (function () { | ||||
| 	 * an ugly top margin. By using this function, the top bar will be hidden if the | ||||
| 	 * update notification is not visible. | ||||
| 	 */ | ||||
| 	var updateWrapperStates = function () { | ||||
| 		var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"]; | ||||
| 	const updateWrapperStates = function () { | ||||
| 		const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"]; | ||||
|  | ||||
| 		positions.forEach(function (position) { | ||||
| 			var wrapper = selectWrapper(position); | ||||
| 			var moduleWrappers = wrapper.getElementsByClassName("module"); | ||||
| 			const wrapper = selectWrapper(position); | ||||
| 			const moduleWrappers = wrapper.getElementsByClassName("module"); | ||||
|  | ||||
| 			var showWrapper = false; | ||||
| 			let showWrapper = false; | ||||
| 			Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) { | ||||
| 				if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") { | ||||
| 					showWrapper = true; | ||||
| @@ -364,7 +477,7 @@ var MM = (function () { | ||||
| 	/** | ||||
| 	 * Loads the core config and combines it with the system defaults. | ||||
| 	 */ | ||||
| 	var loadConfig = function () { | ||||
| 	const loadConfig = function () { | ||||
| 		// FIXME: Think about how to pass config around without breaking tests | ||||
| 		/* eslint-disable */ | ||||
| 		if (typeof config === "undefined") { | ||||
| @@ -379,51 +492,43 @@ var MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Adds special selectors on a collection of modules. | ||||
| 	 * | ||||
| 	 * @param {Module[]} modules Array of modules. | ||||
| 	 */ | ||||
| 	var setSelectionMethodsForModules = function (modules) { | ||||
| 	const setSelectionMethodsForModules = function (modules) { | ||||
| 		/** | ||||
| 		 * Filter modules with the specified classes. | ||||
| 		 * | ||||
| 		 * @param {string|string[]} className one or multiple classnames (array or space divided). | ||||
| 		 * | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| 		 */ | ||||
| 		var withClass = function (className) { | ||||
| 		const withClass = function (className) { | ||||
| 			return modulesByClass(className, true); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Filter modules without the specified classes. | ||||
| 		 * | ||||
| 		 * @param {string|string[]} className one or multiple classnames (array or space divided). | ||||
| 		 * | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| 		 */ | ||||
| 		var exceptWithClass = function (className) { | ||||
| 		const exceptWithClass = function (className) { | ||||
| 			return modulesByClass(className, false); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Filters a collection of modules based on classname(s). | ||||
| 		 * | ||||
| 		 * @param {string|string[]} className one or multiple classnames (array or space divided). | ||||
| 		 * @param {boolean} include if the filter should include or exclude the modules with the specific classes. | ||||
| 		 * | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| 		 */ | ||||
| 		var modulesByClass = function (className, include) { | ||||
| 			var searchClasses = className; | ||||
| 		const modulesByClass = function (className, include) { | ||||
| 			let searchClasses = className; | ||||
| 			if (typeof className === "string") { | ||||
| 				searchClasses = className.split(" "); | ||||
| 			} | ||||
|  | ||||
| 			var newModules = modules.filter(function (module) { | ||||
| 				var classes = module.data.classes.toLowerCase().split(" "); | ||||
| 			const newModules = modules.filter(function (module) { | ||||
| 				const classes = module.data.classes.toLowerCase().split(" "); | ||||
|  | ||||
| 				for (var c in searchClasses) { | ||||
| 					var searchClass = searchClasses[c]; | ||||
| 				for (const searchClass of searchClasses) { | ||||
| 					if (classes.indexOf(searchClass.toLowerCase()) !== -1) { | ||||
| 						return include; | ||||
| 					} | ||||
| @@ -438,13 +543,11 @@ var MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Removes a module instance from the collection. | ||||
| 		 * | ||||
| 		 * @param {object} module The module instance to remove from the collection. | ||||
| 		 * | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| 		 */ | ||||
| 		var exceptModule = function (module) { | ||||
| 			var newModules = modules.filter(function (mod) { | ||||
| 		const exceptModule = function (module) { | ||||
| 			const newModules = modules.filter(function (mod) { | ||||
| 				return mod.identifier !== module.identifier; | ||||
| 			}); | ||||
|  | ||||
| @@ -454,10 +557,9 @@ var MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Walks thru a collection of modules and executes the callback with the module as an argument. | ||||
| 		 * | ||||
| 		 * @param {Function} callback The function to execute with the module as an argument. | ||||
| 		 */ | ||||
| 		var enumerate = function (callback) { | ||||
| 		const enumerate = function (callback) { | ||||
| 			modules.map(function (module) { | ||||
| 				callback(module); | ||||
| 			}); | ||||
| @@ -483,34 +585,53 @@ var MM = (function () { | ||||
| 		/** | ||||
| 		 * Main init method. | ||||
| 		 */ | ||||
| 		init: function () { | ||||
| 			Log.info("Initializing MagicMirror."); | ||||
| 		init: async function () { | ||||
| 			Log.info("Initializing MagicMirror²."); | ||||
| 			loadConfig(); | ||||
|  | ||||
| 			Log.setLogLevel(config.logLevel); | ||||
|  | ||||
| 			Translator.loadCoreTranslations(config.language); | ||||
| 			Loader.loadModules(); | ||||
| 			await Translator.loadCoreTranslations(config.language); | ||||
| 			await Loader.loadModules(); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets called when all modules are started. | ||||
| 		 * | ||||
| 		 * @param {Module[]} moduleObjects All module instances. | ||||
| 		 */ | ||||
| 		modulesStarted: function (moduleObjects) { | ||||
| 			modules = []; | ||||
| 			let startUp = ""; | ||||
|  | ||||
| 			moduleObjects.forEach((module) => modules.push(module)); | ||||
|  | ||||
| 			Log.info("All modules started!"); | ||||
| 			sendNotification("ALL_MODULES_STARTED"); | ||||
|  | ||||
| 			createDomObjects(); | ||||
|  | ||||
| 			if (config.reloadAfterServerRestart) { | ||||
| 				setInterval(async () => { | ||||
| 					// if server startup time has changed (which means server was restarted) | ||||
| 					// the client reloads the mm page | ||||
| 					try { | ||||
| 						const res = await fetch(`${location.protocol}//${location.host}/startup`); | ||||
| 						const curr = await res.text(); | ||||
| 						if (startUp === "") startUp = curr; | ||||
| 						if (startUp !== curr) { | ||||
| 							startUp = ""; | ||||
| 							window.location.reload(true); | ||||
| 							console.warn("Refreshing Website because server was restarted"); | ||||
| 						} | ||||
| 					} catch (err) { | ||||
| 						Log.error(`MagicMirror not reachable: ${err}`); | ||||
| 					} | ||||
| 				}, config.checkServerInterval); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Send a notification to all modules. | ||||
| 		 * | ||||
| 		 * @param {string} notification The identifier of the notification. | ||||
| 		 * @param {*} payload The payload of the notification. | ||||
| 		 * @param {Module} sender The module that sent the notification. | ||||
| @@ -537,23 +658,26 @@ var MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Update the dom for a specific module. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module that needs an update. | ||||
| 		 * @param {number} [speed] The number of microseconds for the animation. | ||||
| 		 * @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates) | ||||
| 		 */ | ||||
| 		updateDom: function (module, speed) { | ||||
| 		updateDom: function (module, updateOptions) { | ||||
| 			if (!(module instanceof Module)) { | ||||
| 				Log.error("updateDom: Sender should be a module."); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			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); | ||||
| 			updateDom(module, updateOptions); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Returns a collection of all modules currently active. | ||||
| 		 * | ||||
| 		 * @returns {Module[]} A collection of all modules currently active. | ||||
| 		 */ | ||||
| 		getModules: function () { | ||||
| @@ -563,7 +687,6 @@ var MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Hide the module. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module to hide. | ||||
| 		 * @param {number} speed The speed of the hide animation. | ||||
| 		 * @param {Function} callback Called when the animation is done. | ||||
| @@ -576,7 +699,6 @@ var MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Show the module. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module to show. | ||||
| 		 * @param {number} speed The speed of the show animation. | ||||
| 		 * @param {Function} callback Called when the animation is done. | ||||
| @@ -597,11 +719,11 @@ if (typeof Object.assign !== "function") { | ||||
| 			if (target === undefined || target === null) { | ||||
| 				throw new TypeError("Cannot convert undefined or null to object"); | ||||
| 			} | ||||
| 			var output = Object(target); | ||||
| 			for (var index = 1; index < arguments.length; index++) { | ||||
| 				var source = arguments[index]; | ||||
| 			const output = Object(target); | ||||
| 			for (let index = 1; index < arguments.length; index++) { | ||||
| 				const source = arguments[index]; | ||||
| 				if (source !== undefined && source !== null) { | ||||
| 					for (var nextKey in source) { | ||||
| 					for (const nextKey in source) { | ||||
| 						if (source.hasOwnProperty(nextKey)) { | ||||
| 							output[nextKey] = source[nextKey]; | ||||
| 						} | ||||
|   | ||||
							
								
								
									
										252
									
								
								js/module.js
									
									
									
									
									
								
							
							
						
						
									
										252
									
								
								js/module.js
									
									
									
									
									
								
							| @@ -1,19 +1,18 @@ | ||||
| /* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Module Blueprint. | ||||
|  * @typedef {Object} Module | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  * | ||||
|  */ | ||||
| var Module = Class.extend({ | ||||
| const Module = Class.extend({ | ||||
| 	/********************************************************* | ||||
| 	 * All methods (and properties) below can be subclassed. * | ||||
| 	 *********************************************************/ | ||||
|  | ||||
| 	// Set the minimum MagicMirror module version for this module. | ||||
| 	// Set the minimum MagicMirror² module version for this module. | ||||
| 	requiresVersion: "2.0.0", | ||||
|  | ||||
| 	// Module config defaults. | ||||
| @@ -26,7 +25,7 @@ var Module = Class.extend({ | ||||
| 	// visibility when hiding and showing module. | ||||
| 	lockStrings: [], | ||||
|  | ||||
| 	// Storage of the nunjuck Environment, | ||||
| 	// Storage of the nunjucks Environment, | ||||
| 	// This should not be referenced directly. | ||||
| 	// Use the nunjucksEnvironment() to get it. | ||||
| 	_nunjucksEnvironment: null, | ||||
| @@ -41,13 +40,12 @@ var Module = Class.extend({ | ||||
| 	/** | ||||
| 	 * Called when the module is started. | ||||
| 	 */ | ||||
| 	start: function () { | ||||
| 		Log.info("Starting module: " + this.name); | ||||
| 	start: async function () { | ||||
| 		Log.info(`Starting module: ${this.name}`); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns a list of scripts the module requires to be loaded. | ||||
| 	 * | ||||
| 	 * @returns {string[]} An array with filenames. | ||||
| 	 */ | ||||
| 	getScripts: function () { | ||||
| @@ -56,7 +54,6 @@ var Module = Class.extend({ | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns a list of stylesheets the module requires to be loaded. | ||||
| 	 * | ||||
| 	 * @returns {string[]} An array with filenames. | ||||
| 	 */ | ||||
| 	getStyles: function () { | ||||
| @@ -67,7 +64,6 @@ var Module = Class.extend({ | ||||
| 	 * Returns a map of translation files the module requires to be loaded. | ||||
| 	 * | ||||
| 	 * return Map<String, String> - | ||||
| 	 * | ||||
| 	 * @returns {*} A map with langKeys and filenames. | ||||
| 	 */ | ||||
| 	getTranslations: function () { | ||||
| @@ -75,23 +71,21 @@ var Module = Class.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Generates the dom which needs to be displayed. This method is called by the Magic Mirror core. | ||||
| 	 * Generates the dom which needs to be displayed. This method is called by the MagicMirror² core. | ||||
| 	 * This method can to be subclassed if the module wants to display info on the mirror. | ||||
| 	 * Alternatively, the getTemplate method could be subclassed. | ||||
| 	 * | ||||
| 	 * @returns {HTMLElement|Promise} The dom or a promise with the dom to display. | ||||
| 	 */ | ||||
| 	getDom: function () { | ||||
| 		var self = this; | ||||
| 		return new Promise(function (resolve) { | ||||
| 			var div = document.createElement("div"); | ||||
| 			var template = self.getTemplate(); | ||||
| 			var templateData = self.getTemplateData(); | ||||
| 		return new Promise((resolve) => { | ||||
| 			const div = document.createElement("div"); | ||||
| 			const template = this.getTemplate(); | ||||
| 			const templateData = this.getTemplateData(); | ||||
|  | ||||
| 			// Check to see if we need to render a template string or a file. | ||||
| 			if (/^.*((\.html)|(\.njk))$/.test(template)) { | ||||
| 				// the template is a filename | ||||
| 				self.nunjucksEnvironment().render(template, templateData, function (err, res) { | ||||
| 				this.nunjucksEnvironment().render(template, templateData, function (err, res) { | ||||
| 					if (err) { | ||||
| 						Log.error(err); | ||||
| 					} | ||||
| @@ -102,7 +96,7 @@ var Module = Class.extend({ | ||||
| 				}); | ||||
| 			} else { | ||||
| 				// the template is a template string. | ||||
| 				div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData); | ||||
| 				div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData); | ||||
|  | ||||
| 				resolve(div); | ||||
| 			} | ||||
| @@ -111,9 +105,8 @@ var Module = Class.extend({ | ||||
|  | ||||
| 	/** | ||||
| 	 * Generates the header string which needs to be displayed if a user has a header configured for this module. | ||||
| 	 * This method is called by the Magic Mirror core, but only if the user has configured a default header for the module. | ||||
| 	 * This method is called by the MagicMirror² core, but only if the user has configured a default header for the module. | ||||
| 	 * This method needs to be subclassed if the module wants to display modified headers on the mirror. | ||||
| 	 * | ||||
| 	 * @returns {string} The header to display above the header. | ||||
| 	 */ | ||||
| 	getHeader: function () { | ||||
| @@ -125,17 +118,15 @@ var Module = Class.extend({ | ||||
| 	 * This method needs to be subclassed if the module wants to use a template. | ||||
| 	 * It can either return a template sting, or a template filename. | ||||
| 	 * If the string ends with '.html' it's considered a file from within the module's folder. | ||||
| 	 * | ||||
| 	 * @returns {string} The template string of filename. | ||||
| 	 */ | ||||
| 	getTemplate: function () { | ||||
| 		return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>"; | ||||
| 		return `<div class="normal">${this.name}</div><div class="small dimmed">${this.identifier}</div>`; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns the data to be used in the template. | ||||
| 	 * This method needs to be subclassed if the module wants to use a custom data. | ||||
| 	 * | ||||
| 	 * @returns {object} The data for the template | ||||
| 	 */ | ||||
| 	getTemplateData: function () { | ||||
| @@ -143,8 +134,7 @@ var Module = Class.extend({ | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Called by the Magic Mirror core when a notification arrives. | ||||
| 	 * | ||||
| 	 * Called by the MagicMirror² core when a notification arrives. | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 * @param {Module} sender The module that sent the notification. | ||||
| @@ -160,7 +150,6 @@ var Module = Class.extend({ | ||||
| 	/** | ||||
| 	 * Returns the nunjucks environment for the current module. | ||||
| 	 * The environment is checked in the _nunjucksEnvironment instance variable. | ||||
| 	 * | ||||
| 	 * @returns {object} The Nunjucks Environment | ||||
| 	 */ | ||||
| 	nunjucksEnvironment: function () { | ||||
| @@ -168,15 +157,13 @@ var Module = Class.extend({ | ||||
| 			return this._nunjucksEnvironment; | ||||
| 		} | ||||
|  | ||||
| 		var self = this; | ||||
|  | ||||
| 		this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), { async: true }), { | ||||
| 			trimBlocks: true, | ||||
| 			lstripBlocks: true | ||||
| 		}); | ||||
|  | ||||
| 		this._nunjucksEnvironment.addFilter("translate", function (str, variables) { | ||||
| 			return self.translate(str, variables); | ||||
| 		this._nunjucksEnvironment.addFilter("translate", (str, variables) => { | ||||
| 			return nunjucks.runtime.markSafe(this.translate(str, variables)); | ||||
| 		}); | ||||
|  | ||||
| 		return this._nunjucksEnvironment; | ||||
| @@ -184,49 +171,48 @@ var Module = Class.extend({ | ||||
|  | ||||
| 	/** | ||||
| 	 * Called when a socket notification arrives. | ||||
| 	 * | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 */ | ||||
| 	socketNotificationReceived: function (notification, payload) { | ||||
| 		Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload); | ||||
| 		Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`); | ||||
| 	}, | ||||
|  | ||||
| 	/* | ||||
| 	/** | ||||
| 	 * Called when the module is hidden. | ||||
| 	 */ | ||||
| 	suspend: function () { | ||||
| 		Log.log(this.name + " is suspended."); | ||||
| 		Log.log(`${this.name} is suspended.`); | ||||
| 	}, | ||||
|  | ||||
| 	/* | ||||
| 	/** | ||||
| 	 * Called when the module is shown. | ||||
| 	 */ | ||||
| 	resume: function () { | ||||
| 		Log.log(this.name + " is resumed."); | ||||
| 		Log.log(`${this.name} is resumed.`); | ||||
| 	}, | ||||
|  | ||||
| 	/********************************************* | ||||
| 	 * The methods below don"t need subclassing. * | ||||
| 	 * The methods below don't need subclassing. * | ||||
| 	 *********************************************/ | ||||
|  | ||||
| 	/** | ||||
| 	 * Set the module data. | ||||
| 	 * | ||||
| 	 * @param {Module} data The module data | ||||
| 	 * @param {object} data The module data | ||||
| 	 */ | ||||
| 	setData: function (data) { | ||||
| 		this.data = data; | ||||
| 		this.name = data.name; | ||||
| 		this.identifier = data.identifier; | ||||
| 		this.hidden = false; | ||||
| 		this.hasAnimateIn = false; | ||||
| 		this.hasAnimateOut = false; | ||||
|  | ||||
| 		this.setConfig(data.config, data.configDeepMerge); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Set the module config and combine it with the module defaults. | ||||
| 	 * | ||||
| 	 * @param {object} config The combined module config. | ||||
| 	 * @param {boolean} deep Merge module config in deep. | ||||
| 	 */ | ||||
| @@ -237,7 +223,6 @@ var Module = Class.extend({ | ||||
| 	/** | ||||
| 	 * Returns a socket object. If it doesn't exist, it's created. | ||||
| 	 * It also registers the notification callback. | ||||
| 	 * | ||||
| 	 * @returns {MMSocket} a socket object | ||||
| 	 */ | ||||
| 	socket: function () { | ||||
| @@ -245,9 +230,8 @@ var Module = Class.extend({ | ||||
| 			this._socket = new MMSocket(this.name); | ||||
| 		} | ||||
|  | ||||
| 		var self = this; | ||||
| 		this._socket.setNotificationCallback(function (notification, payload) { | ||||
| 			self.socketNotificationReceived(notification, payload); | ||||
| 		this._socket.setNotificationCallback((notification, payload) => { | ||||
| 			this.socketNotificationReceived(notification, payload); | ||||
| 		}); | ||||
|  | ||||
| 		return this._socket; | ||||
| @@ -255,94 +239,82 @@ var Module = Class.extend({ | ||||
|  | ||||
| 	/** | ||||
| 	 * Retrieve the path to a module file. | ||||
| 	 * | ||||
| 	 * @param {string} file Filename | ||||
| 	 * @returns {string} the file path | ||||
| 	 */ | ||||
| 	file: function (file) { | ||||
| 		return (this.data.path + "/" + file).replace("//", "/"); | ||||
| 		return `${this.data.path}/${file}`.replace("//", "/"); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Load all required stylesheets by requesting the MM object to load the files. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadStyles: function (callback) { | ||||
| 		this.loadDependencies("getStyles", callback); | ||||
| 	loadStyles: function () { | ||||
| 		return this.loadDependencies("getStyles"); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Load all required scripts by requesting the MM object to load the files. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadScripts: function (callback) { | ||||
| 		this.loadDependencies("getScripts", callback); | ||||
| 	loadScripts: function () { | ||||
| 		return this.loadDependencies("getScripts"); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Helper method to load all dependencies. | ||||
| 	 * | ||||
| 	 * @param {string} funcName Function name to call to get scripts or styles. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadDependencies: function (funcName, callback) { | ||||
| 		var self = this; | ||||
| 		var dependencies = this[funcName](); | ||||
| 	loadDependencies: async function (funcName) { | ||||
| 		let dependencies = this[funcName](); | ||||
|  | ||||
| 		var loadNextDependency = function () { | ||||
| 		const loadNextDependency = async () => { | ||||
| 			if (dependencies.length > 0) { | ||||
| 				var nextDependency = dependencies[0]; | ||||
| 				Loader.loadFile(nextDependency, self, function () { | ||||
| 					dependencies = dependencies.slice(1); | ||||
| 					loadNextDependency(); | ||||
| 				}); | ||||
| 				const nextDependency = dependencies[0]; | ||||
| 				await Loader.loadFileForModule(nextDependency, this); | ||||
| 				dependencies = dependencies.slice(1); | ||||
| 				await loadNextDependency(); | ||||
| 			} else { | ||||
| 				callback(); | ||||
| 				return Promise.resolve(); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		loadNextDependency(); | ||||
| 		await loadNextDependency(); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Load all translations. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadTranslations: function (callback) { | ||||
| 		var self = this; | ||||
| 		var translations = this.getTranslations(); | ||||
| 		var lang = config.language.toLowerCase(); | ||||
| 	loadTranslations: async function () { | ||||
| 		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 (languages.length === 0) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (translations) { | ||||
| 			var translationFile = translations[lang] || undefined; | ||||
| 			var translationsFallbackFile = translations[first]; | ||||
| 		const translationFile = translations[language]; | ||||
| 		const translationsFallbackFile = translations[fallbackLanguage]; | ||||
|  | ||||
| 			// 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 { | ||||
| 			callback(); | ||||
| 		if (!translationFile) { | ||||
| 			return Translator.load(this, translationsFallbackFile, true); | ||||
| 		} | ||||
|  | ||||
| 		await Translator.load(this, translationFile, false); | ||||
|  | ||||
| 		if (translationFile !== translationsFallbackFile) { | ||||
| 			return Translator.load(this, translationsFallbackFile, true); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Request the translation for a given key with optional variables and default value. | ||||
| 	 * | ||||
| 	 * @param {string} key The key of the string to translate | ||||
| 	 * @param {string|object} [defaultValueOrVariables] The default value or variables for translating. | ||||
| 	 * @param {string} [defaultValue] The default value with variables. | ||||
| @@ -357,16 +329,14 @@ var Module = Class.extend({ | ||||
|  | ||||
| 	/** | ||||
| 	 * Request an (animated) update of the module. | ||||
| 	 * | ||||
| 	 * @param {number} [speed] The speed of the animation. | ||||
| 	 * @param {number|object} [updateOptions] The speed of the animation or object with for updateOptions (speed/animates) | ||||
| 	 */ | ||||
| 	updateDom: function (speed) { | ||||
| 		MM.updateDom(this, speed); | ||||
| 	updateDom: function (updateOptions) { | ||||
| 		MM.updateDom(this, updateOptions); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Send a notification to all modules. | ||||
| 	 * | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 */ | ||||
| @@ -376,7 +346,6 @@ var Module = Class.extend({ | ||||
|  | ||||
| 	/** | ||||
| 	 * Send a socket notification to the node helper. | ||||
| 	 * | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 */ | ||||
| @@ -386,63 +355,61 @@ var Module = Class.extend({ | ||||
|  | ||||
| 	/** | ||||
| 	 * Hide this module. | ||||
| 	 * | ||||
| 	 * @param {number} speed The speed of the hide animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the hide method. | ||||
| 	 */ | ||||
| 	hide: function (speed, callback, options) { | ||||
| 	hide: function (speed, callback, options = {}) { | ||||
| 		let usedCallback = callback || function () {}; | ||||
| 		let usedOptions = options; | ||||
|  | ||||
| 		if (typeof callback === "object") { | ||||
| 			options = callback; | ||||
| 			callback = function () {}; | ||||
| 			Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!"); | ||||
| 			usedOptions = callback; | ||||
| 			usedCallback = function () {}; | ||||
| 		} | ||||
|  | ||||
| 		callback = callback || function () {}; | ||||
| 		options = options || {}; | ||||
|  | ||||
| 		var self = this; | ||||
| 		MM.hideModule( | ||||
| 			self, | ||||
| 			this, | ||||
| 			speed, | ||||
| 			function () { | ||||
| 				self.suspend(); | ||||
| 				callback(); | ||||
| 			() => { | ||||
| 				this.suspend(); | ||||
| 				usedCallback(); | ||||
| 			}, | ||||
| 			options | ||||
| 			usedOptions | ||||
| 		); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Show this module. | ||||
| 	 * | ||||
| 	 * @param {number} speed The speed of the show animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the show method. | ||||
| 	 */ | ||||
| 	show: function (speed, callback, options) { | ||||
| 		let usedCallback = callback || function () {}; | ||||
| 		let usedOptions = options; | ||||
|  | ||||
| 		if (typeof callback === "object") { | ||||
| 			options = callback; | ||||
| 			callback = function () {}; | ||||
| 			Log.error("Parameter mismatch in module.show: callback is not an optional parameter!"); | ||||
| 			usedOptions = callback; | ||||
| 			usedCallback = function () {}; | ||||
| 		} | ||||
|  | ||||
| 		callback = callback || function () {}; | ||||
| 		options = options || {}; | ||||
|  | ||||
| 		var self = this; | ||||
| 		MM.showModule( | ||||
| 			this, | ||||
| 			speed, | ||||
| 			function () { | ||||
| 				self.resume(); | ||||
| 				callback(); | ||||
| 			() => { | ||||
| 				this.resume(); | ||||
| 				usedCallback(); | ||||
| 			}, | ||||
| 			options | ||||
| 			usedOptions | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Merging MagicMirror (or other) default/config script by @bugsounet | ||||
|  * Merging MagicMirror² (or other) default/config script by @bugsounet | ||||
|  * Merge 2 objects or/with array | ||||
|  * | ||||
|  * Usage: | ||||
| @@ -460,14 +427,13 @@ var Module = Class.extend({ | ||||
|  * ------- | ||||
|  * | ||||
|  * Todo: idea of Mich determinate what do you want to merge or not | ||||
|  * | ||||
|  * @param {object} result the initial object | ||||
|  * @returns {object} the merged config | ||||
|  */ | ||||
| function configMerge(result) { | ||||
| 	var stack = Array.prototype.slice.call(arguments, 1); | ||||
| 	var item; | ||||
| 	var key; | ||||
| 	const stack = Array.prototype.slice.call(arguments, 1); | ||||
| 	let item, key; | ||||
|  | ||||
| 	while (stack.length) { | ||||
| 		item = stack.shift(); | ||||
| 		for (key in item) { | ||||
| @@ -495,46 +461,46 @@ Module.create = function (name) { | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	var moduleDefinition = Module.definitions[name]; | ||||
| 	var clonedDefinition = cloneObject(moduleDefinition); | ||||
| 	const moduleDefinition = Module.definitions[name]; | ||||
| 	const clonedDefinition = cloneObject(moduleDefinition); | ||||
|  | ||||
| 	// Note that we clone the definition. Otherwise the objects are shared, which gives problems. | ||||
| 	var ModuleClass = Module.extend(clonedDefinition); | ||||
| 	const ModuleClass = Module.extend(clonedDefinition); | ||||
|  | ||||
| 	return new ModuleClass(); | ||||
| }; | ||||
|  | ||||
| Module.register = function (name, moduleDefinition) { | ||||
| 	if (moduleDefinition.requiresVersion) { | ||||
| 		Log.log("Check MagicMirror version for module '" + name + "' - Minimum version:  " + moduleDefinition.requiresVersion + " - Current version: " + window.version); | ||||
| 		if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) { | ||||
| 		Log.log(`Check MagicMirror² version for module '${name}' - Minimum version:  ${moduleDefinition.requiresVersion} - Current version: ${window.mmVersion}`); | ||||
| 		if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) { | ||||
| 			Log.log("Version is ok!"); | ||||
| 		} else { | ||||
| 			Log.warn("Version is incorrect. Skip module: '" + name + "'"); | ||||
| 			Log.warn(`Version is incorrect. Skip module: '${name}'`); | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
| 	Log.log("Module registered: " + name); | ||||
| 	Log.log(`Module registered: ${name}`); | ||||
| 	Module.definitions[name] = moduleDefinition; | ||||
| }; | ||||
|  | ||||
| window.Module = Module; | ||||
|  | ||||
| /** | ||||
|  * Compare two semantic version numbers and return the difference. | ||||
|  * | ||||
|  * @param {string} a Version number a. | ||||
|  * @param {string} b Version number b. | ||||
|  * @returns {number} A positive number if a is larger than b, a negative | ||||
|  * number if a is smaller and 0 if they are the same | ||||
|  */ | ||||
| function cmpVersions(a, b) { | ||||
| 	var i, diff; | ||||
| 	var regExStrip0 = /(\.0+)+$/; | ||||
| 	var segmentsA = a.replace(regExStrip0, "").split("."); | ||||
| 	var segmentsB = b.replace(regExStrip0, "").split("."); | ||||
| 	var l = Math.min(segmentsA.length, segmentsB.length); | ||||
| 	const regExStrip0 = /(\.0+)+$/; | ||||
| 	const segmentsA = a.replace(regExStrip0, "").split("."); | ||||
| 	const segmentsB = b.replace(regExStrip0, "").split("."); | ||||
| 	const l = Math.min(segmentsA.length, segmentsB.length); | ||||
|  | ||||
| 	for (i = 0; i < l; i++) { | ||||
| 		diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10); | ||||
| 	for (let i = 0; i < l; i++) { | ||||
| 		let diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10); | ||||
| 		if (diff) { | ||||
| 			return diff; | ||||
| 		} | ||||
|   | ||||
| @@ -1,62 +1,57 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Node Helper Superclass | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const Class = require("./class.js"); | ||||
| const Log = require("./logger.js"); | ||||
| const express = require("express"); | ||||
| const Log = require("logger"); | ||||
| const Class = require("./class"); | ||||
|  | ||||
| var NodeHelper = Class.extend({ | ||||
| 	init: function () { | ||||
| const NodeHelper = Class.extend({ | ||||
| 	init() { | ||||
| 		Log.log("Initializing new module helper ..."); | ||||
| 	}, | ||||
|  | ||||
| 	loaded: function (callback) { | ||||
| 		Log.log("Module helper loaded: " + this.name); | ||||
| 		callback(); | ||||
| 	loaded() { | ||||
| 		Log.log(`Module helper loaded: ${this.name}`); | ||||
| 	}, | ||||
|  | ||||
| 	start: function () { | ||||
| 		Log.log("Starting module helper: " + this.name); | ||||
| 	start() { | ||||
| 		Log.log(`Starting module helper: ${this.name}`); | ||||
| 	}, | ||||
|  | ||||
| 	/* stop() | ||||
| 	 * Called when the MagicMirror server receives a `SIGINT` | ||||
| 	/** | ||||
| 	 * Called when the MagicMirror² server receives a `SIGINT` | ||||
| 	 * Close any open connections, stop any sub-processes and | ||||
| 	 * gracefully exit the module. | ||||
| 	 * | ||||
| 	 */ | ||||
| 	stop: function () { | ||||
| 		Log.log("Stopping module helper: " + this.name); | ||||
| 	stop() { | ||||
| 		Log.log(`Stopping module helper: ${this.name}`); | ||||
| 	}, | ||||
|  | ||||
| 	/* socketNotificationReceived(notification, payload) | ||||
| 	/** | ||||
| 	 * This method is called when a socket notification arrives. | ||||
| 	 * | ||||
| 	 * argument notification string - The identifier of the notification. | ||||
| 	 * argument payload mixed - The payload of the notification. | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*}  payload The payload of the notification. | ||||
| 	 */ | ||||
| 	socketNotificationReceived: function (notification, payload) { | ||||
| 		Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload); | ||||
| 	socketNotificationReceived(notification, payload) { | ||||
| 		Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`); | ||||
| 	}, | ||||
|  | ||||
| 	/* setName(name) | ||||
| 	/** | ||||
| 	 * Set the module name. | ||||
| 	 * | ||||
| 	 * argument name string - Module name. | ||||
| 	 * @param {string} name Module name. | ||||
| 	 */ | ||||
| 	setName: function (name) { | ||||
| 	setName(name) { | ||||
| 		this.name = name; | ||||
| 	}, | ||||
|  | ||||
| 	/* setPath(path) | ||||
| 	/** | ||||
| 	 * Set the module path. | ||||
| 	 * | ||||
| 	 * argument path string - Module path. | ||||
| 	 * @param {string} path Module path. | ||||
| 	 */ | ||||
| 	setPath: function (path) { | ||||
| 	setPath(path) { | ||||
| 		this.path = path; | ||||
| 	}, | ||||
|  | ||||
| @@ -66,7 +61,7 @@ var 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); | ||||
| 	}, | ||||
|  | ||||
| @@ -76,11 +71,10 @@ var 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) | ||||
| @@ -89,38 +83,58 @@ var 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; | ||||
|  | ||||
| 		Log.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; | ||||
| 			const onevent = socket.onevent; | ||||
| 			socket.onevent = function (packet) { | ||||
| 				var args = packet.data || []; | ||||
| 				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 !== "*") { | ||||
| 					//Log.log('received message in namespace: ' + namespace); | ||||
| 					self.socketNotificationReceived(notification, payload); | ||||
| 					this.socketNotificationReceived(notification, payload); | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| 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); | ||||
| }; | ||||
|  | ||||
| /*************** DO NOT EDIT THE LINE BELOW ***************/ | ||||
| if (typeof module !== "undefined") { | ||||
| 	module.exports = NodeHelper; | ||||
| } | ||||
| module.exports = NodeHelper; | ||||
|   | ||||
							
								
								
									
										183
									
								
								js/server.js
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								js/server.js
									
									
									
									
									
								
							| @@ -1,88 +1,121 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Server | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| 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"); | ||||
| const fs = require("fs"); | ||||
| const http = require("http"); | ||||
| const https = require("https"); | ||||
| const path = require("path"); | ||||
| const express = require("express"); | ||||
| const ipfilter = require("express-ipfilter").IpFilter; | ||||
| const helmet = require("helmet"); | ||||
| const socketio = require("socket.io"); | ||||
|  | ||||
| var Log = require("./logger.js"); | ||||
| var Utils = require("./utils.js"); | ||||
| const Log = require("logger"); | ||||
| const Utils = require("./utils"); | ||||
| const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions"); | ||||
|  | ||||
| var Server = function (config, callback) { | ||||
| 	var port = config.port; | ||||
| 	if (process.env.MM_PORT) { | ||||
| 		port = process.env.MM_PORT; | ||||
| 	} | ||||
| /** | ||||
|  * Server | ||||
|  * @param {object} config The MM config | ||||
|  * @class | ||||
|  */ | ||||
| function Server(config) { | ||||
| 	const app = express(); | ||||
| 	const port = process.env.MM_PORT || config.port; | ||||
| 	const serverSockets = new Set(); | ||||
| 	let server = null; | ||||
|  | ||||
| 	var server = null; | ||||
| 	if (config.useHttps) { | ||||
| 		var options = { | ||||
| 			key: fs.readFileSync(config.httpsPrivateKey), | ||||
| 			cert: fs.readFileSync(config.httpsCertificate) | ||||
| 		}; | ||||
| 		server = require("https").Server(options, app); | ||||
| 	} else { | ||||
| 		server = require("http").Server(app); | ||||
| 	} | ||||
| 	var io = require("socket.io")(server); | ||||
|  | ||||
| 	Log.log("Starting server on port " + port + " ... "); | ||||
|  | ||||
| 	server.listen(port, config.address ? config.address : "localhost"); | ||||
|  | ||||
| 	if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) { | ||||
| 		Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs")); | ||||
| 	} | ||||
|  | ||||
| 	app.use(function (req, res, next) { | ||||
| 		var result = ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) { | ||||
| 			if (err === undefined) { | ||||
| 				return next(); | ||||
| 	/** | ||||
| 	 * Opens the server for incoming connections | ||||
| 	 * @returns {Promise} A promise that is resolved when the server listens to connections | ||||
| 	 */ | ||||
| 	this.open = function () { | ||||
| 		return new Promise((resolve) => { | ||||
| 			if (config.useHttps) { | ||||
| 				const options = { | ||||
| 					key: fs.readFileSync(config.httpsPrivateKey), | ||||
| 					cert: fs.readFileSync(config.httpsCertificate) | ||||
| 				}; | ||||
| 				server = https.Server(options, app); | ||||
| 			} else { | ||||
| 				server = http.Server(app); | ||||
| 			} | ||||
| 			Log.log(err.message); | ||||
| 			res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this."); | ||||
| 			const io = socketio(server, { | ||||
| 				cors: { | ||||
| 					origin: /.*$/, | ||||
| 					credentials: true | ||||
| 				}, | ||||
| 				allowEIO3: true | ||||
| 			}); | ||||
|  | ||||
| 			server.on("connection", (socket) => { | ||||
| 				serverSockets.add(socket); | ||||
| 				socket.on("close", () => { | ||||
| 					serverSockets.delete(socket); | ||||
| 				}); | ||||
| 			}); | ||||
|  | ||||
| 			Log.log(`Starting server on port ${port} ... `); | ||||
| 			server.listen(port, config.address || "localhost"); | ||||
|  | ||||
| 			if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) { | ||||
| 				Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs")); | ||||
| 			} | ||||
|  | ||||
| 			app.use(function (req, res, next) { | ||||
| 				ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) { | ||||
| 					if (err === undefined) { | ||||
| 						res.header("Access-Control-Allow-Origin", "*"); | ||||
| 						return next(); | ||||
| 					} | ||||
| 					Log.log(err.message); | ||||
| 					res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this."); | ||||
| 				}); | ||||
| 			}); | ||||
|  | ||||
| 			app.use(helmet(config.httpHeaders)); | ||||
| 			app.use("/js", express.static(__dirname)); | ||||
|  | ||||
| 			// TODO add tests directory only when running tests? | ||||
| 			const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"]; | ||||
| 			for (const directory of directories) { | ||||
| 				app.use(directory, express.static(path.resolve(global.root_path + directory))); | ||||
| 			} | ||||
|  | ||||
| 			app.get("/cors", async (req, res) => await cors(req, res)); | ||||
|  | ||||
| 			app.get("/version", (req, res) => getVersion(req, res)); | ||||
|  | ||||
| 			app.get("/config", (req, res) => getConfig(req, res)); | ||||
|  | ||||
| 			app.get("/startup", (req, res) => getStartup(req, res)); | ||||
|  | ||||
| 			app.get("/", (req, res) => getHtml(req, res)); | ||||
|  | ||||
| 			server.on("listening", () => { | ||||
| 				resolve({ | ||||
| 					app, | ||||
| 					io | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| 	app.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]; | ||||
| 		app.use(directory, express.static(path.resolve(global.root_path + directory))); | ||||
| 	} | ||||
|  | ||||
| 	app.get("/version", function (req, res) { | ||||
| 		res.send(global.version); | ||||
| 	}); | ||||
|  | ||||
| 	app.get("/config", function (req, res) { | ||||
| 		res.send(config); | ||||
| 	}); | ||||
|  | ||||
| 	app.get("/", function (req, res) { | ||||
| 		var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), { encoding: "utf8" }); | ||||
| 		html = html.replace("#VERSION#", global.version); | ||||
|  | ||||
| 		var configFile = "config/config.js"; | ||||
| 		if (typeof global.configuration_file !== "undefined") { | ||||
| 			configFile = global.configuration_file; | ||||
| 		} | ||||
| 		html = html.replace("#CONFIG_FILE#", configFile); | ||||
|  | ||||
| 		res.send(html); | ||||
| 	}); | ||||
|  | ||||
| 	if (typeof callback === "function") { | ||||
| 		callback(app, io); | ||||
| 	} | ||||
| }; | ||||
| 	/** | ||||
| 	 * Closes the server and destroys all lingering connections to it. | ||||
| 	 * @returns {Promise} A promise that resolves when server has successfully shut down | ||||
| 	 */ | ||||
| 	this.close = function () { | ||||
| 		return new Promise((resolve) => { | ||||
| 			for (const socket of serverSockets.values()) { | ||||
| 				socket.destroy(); | ||||
| 			} | ||||
| 			server.close(resolve); | ||||
| 		}); | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| module.exports = Server; | ||||
|   | ||||
							
								
								
									
										130
									
								
								js/server_functions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								js/server_functions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| const fs = require("fs"); | ||||
| const path = require("path"); | ||||
| const Log = require("logger"); | ||||
| const startUp = new Date(); | ||||
|  | ||||
| /** | ||||
|  * Gets the config. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getConfig(req, res) { | ||||
| 	res.send(config); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the startup time. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getStartup(req, res) { | ||||
| 	res.send(startUp); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A method that forwards HTTP Get-methods to the internet to avoid CORS-errors. | ||||
|  * | ||||
|  * Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1 | ||||
|  * | ||||
|  * Only the url-param of the input request url is required. It must be the last parameter. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| async function cors(req, res) { | ||||
| 	try { | ||||
| 		const urlRegEx = "url=(.+?)$"; | ||||
| 		let url; | ||||
|  | ||||
| 		const match = new RegExp(urlRegEx, "g").exec(req.url); | ||||
| 		if (!match) { | ||||
| 			url = `invalid url: ${req.url}`; | ||||
| 			Log.error(url); | ||||
| 			res.send(url); | ||||
| 		} else { | ||||
| 			url = match[1]; | ||||
|  | ||||
| 			const headersToSend = getHeadersToSend(req.url); | ||||
| 			const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url); | ||||
|  | ||||
| 			Log.log(`cors url: ${url}`); | ||||
| 			const response = await fetch(url, { headers: headersToSend }); | ||||
|  | ||||
| 			for (const header of expectedRecievedHeaders) { | ||||
| 				const headerValue = response.headers.get(header); | ||||
| 				if (header) res.set(header, headerValue); | ||||
| 			} | ||||
| 			const data = await response.text(); | ||||
| 			res.send(data); | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		Log.error(error); | ||||
| 		res.send(error); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets headers and values to attach to the web request. | ||||
|  * @param {string} url - The url containing the headers and values to send. | ||||
|  * @returns {object} An object specifying name and value of the headers. | ||||
|  */ | ||||
| function getHeadersToSend(url) { | ||||
| 	const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` }; | ||||
| 	const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url); | ||||
| 	if (headersToSendMatch) { | ||||
| 		const headers = headersToSendMatch[1].split(","); | ||||
| 		for (const header of headers) { | ||||
| 			const keyValue = header.split(":"); | ||||
| 			if (keyValue.length !== 2) { | ||||
| 				throw new Error(`Invalid format for header ${header}`); | ||||
| 			} | ||||
| 			headersToSend[keyValue[0]] = decodeURIComponent(keyValue[1]); | ||||
| 		} | ||||
| 	} | ||||
| 	return headersToSend; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the headers expected from the response. | ||||
|  * @param {string} url - The url containing the expected headers from the response. | ||||
|  * @returns {string[]} headers - The name of the expected headers. | ||||
|  */ | ||||
| function geExpectedRecievedHeaders(url) { | ||||
| 	const expectedRecievedHeaders = ["Content-Type"]; | ||||
| 	const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url); | ||||
| 	if (expectedRecievedHeadersMatch) { | ||||
| 		const headers = expectedRecievedHeadersMatch[1].split(","); | ||||
| 		for (const header of headers) { | ||||
| 			expectedRecievedHeaders.push(header); | ||||
| 		} | ||||
| 	} | ||||
| 	return expectedRecievedHeaders; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the HTML to display the magic mirror. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getHtml(req, res) { | ||||
| 	let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" }); | ||||
| 	html = html.replace("#VERSION#", global.version); | ||||
|  | ||||
| 	let configFile = "config/config.js"; | ||||
| 	if (typeof global.configuration_file !== "undefined") { | ||||
| 		configFile = global.configuration_file; | ||||
| 	} | ||||
| 	html = html.replace("#CONFIG_FILE#", configFile); | ||||
|  | ||||
| 	res.send(html); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the MagicMirror version. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getVersion(req, res) { | ||||
| 	res.send(global.version); | ||||
| } | ||||
|  | ||||
| module.exports = { cors, getConfig, getHtml, getVersion, getStartup }; | ||||
| @@ -1,54 +1,50 @@ | ||||
| /* global io */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * TODO add description | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var MMSocket = function (moduleName) { | ||||
| 	var self = this; | ||||
|  | ||||
| const MMSocket = function (moduleName) { | ||||
| 	if (typeof moduleName !== "string") { | ||||
| 		throw new Error("Please set the module name for the MMSocket."); | ||||
| 	} | ||||
|  | ||||
| 	self.moduleName = moduleName; | ||||
| 	this.moduleName = moduleName; | ||||
|  | ||||
| 	// Private Methods | ||||
| 	var base = "/"; | ||||
| 	let base = "/"; | ||||
| 	if (typeof config !== "undefined" && typeof config.basePath !== "undefined") { | ||||
| 		base = config.basePath; | ||||
| 	} | ||||
| 	self.socket = io("/" + self.moduleName, { | ||||
| 		path: base + "socket.io" | ||||
| 	this.socket = io(`/${this.moduleName}`, { | ||||
| 		path: `${base}socket.io` | ||||
| 	}); | ||||
| 	var notificationCallback = function () {}; | ||||
|  | ||||
| 	var onevent = self.socket.onevent; | ||||
| 	self.socket.onevent = function (packet) { | ||||
| 		var args = packet.data || []; | ||||
| 		onevent.call(this, packet); // original call | ||||
| 	let notificationCallback = function () {}; | ||||
|  | ||||
| 	const onevent = this.socket.onevent; | ||||
| 	this.socket.onevent = (packet) => { | ||||
| 		const args = packet.data || []; | ||||
| 		onevent.call(this.socket, packet); // original call | ||||
| 		packet.data = ["*"].concat(args); | ||||
| 		onevent.call(this, packet); // additional call to catch-all | ||||
| 		onevent.call(this.socket, packet); // additional call to catch-all | ||||
| 	}; | ||||
|  | ||||
| 	// register catch all. | ||||
| 	self.socket.on("*", function (notification, payload) { | ||||
| 	this.socket.on("*", (notification, payload) => { | ||||
| 		if (notification !== "*") { | ||||
| 			notificationCallback(notification, payload); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	// Public Methods | ||||
| 	this.setNotificationCallback = function (callback) { | ||||
| 	this.setNotificationCallback = (callback) => { | ||||
| 		notificationCallback = callback; | ||||
| 	}; | ||||
|  | ||||
| 	this.sendNotification = function (notification, payload) { | ||||
| 		if (typeof payload === "undefined") { | ||||
| 			payload = {}; | ||||
| 		} | ||||
| 		self.socket.emit(notification, payload); | ||||
| 	this.sendNotification = (notification, payload = {}) => { | ||||
| 		this.socket.emit(notification, payload); | ||||
| 	}; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										118
									
								
								js/translator.js
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								js/translator.js
									
									
									
									
									
								
							| @@ -1,36 +1,37 @@ | ||||
| /* global translations */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Translator (l10n) | ||||
|  * | ||||
|  * By Christopher Fenner https://github.com/CFenner | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var Translator = (function () { | ||||
| const Translator = (function () { | ||||
| 	/** | ||||
| 	 * Load a JSON file via XHR. | ||||
| 	 * | ||||
| 	 * @param {string} file Path of the file we want to load. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<object>} the translations in the specified file | ||||
| 	 */ | ||||
| 	function loadJSON(file, callback) { | ||||
| 		var xhr = new XMLHttpRequest(); | ||||
| 		xhr.overrideMimeType("application/json"); | ||||
| 		xhr.open("GET", file, true); | ||||
| 		xhr.onreadystatechange = function () { | ||||
| 			if (xhr.readyState === 4 && xhr.status === 200) { | ||||
| 				// needs error handler try/catch at least | ||||
| 				let fileinfo = null; | ||||
| 				try { | ||||
| 					fileinfo = JSON.parse(xhr.responseText); | ||||
| 				} catch (exception) { | ||||
| 					// nothing here, but don't die | ||||
| 					Log.error(" loading json file =" + file + " failed"); | ||||
| 	async function loadJSON(file) { | ||||
| 		const xhr = new XMLHttpRequest(); | ||||
| 		return new Promise(function (resolve) { | ||||
| 			xhr.overrideMimeType("application/json"); | ||||
| 			xhr.open("GET", file, true); | ||||
| 			xhr.onreadystatechange = function () { | ||||
| 				if (xhr.readyState === 4 && xhr.status === 200) { | ||||
| 					// needs error handler try/catch at least | ||||
| 					let fileinfo = null; | ||||
| 					try { | ||||
| 						fileinfo = JSON.parse(xhr.responseText); | ||||
| 					} catch (exception) { | ||||
| 						// nothing here, but don't die | ||||
| 						Log.error(` loading json file =${file} failed`); | ||||
| 					} | ||||
| 					resolve(fileinfo); | ||||
| 				} | ||||
| 				callback(fileinfo); | ||||
| 			} | ||||
| 		}; | ||||
| 		xhr.send(null); | ||||
| 			}; | ||||
| 			xhr.send(null); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	return { | ||||
| @@ -41,21 +42,17 @@ var Translator = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Load a translation for a given key for a given module. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module to load the translation for. | ||||
| 		 * @param {string} key The key of the text to translate. | ||||
| 		 * @param {object} variables The variables to use within the translation template (optional) | ||||
| 		 * @returns {string} the translated key | ||||
| 		 */ | ||||
| 		translate: function (module, key, variables) { | ||||
| 			variables = variables || {}; //Empty object by default | ||||
|  | ||||
| 		translate: function (module, key, variables = {}) { | ||||
| 			/** | ||||
| 			 * Combines template and variables like: | ||||
| 			 * template: "Please wait for {timeToWait} before continuing with {work}." | ||||
| 			 * variables: {timeToWait: "2 hours", work: "painting"} | ||||
| 			 * to: "Please wait for 2 hours before continuing with painting." | ||||
| 			 * | ||||
| 			 * @param {string} template Text with placeholder | ||||
| 			 * @param {object} variables Variables for the placeholder | ||||
| 			 * @returns {string} the template filled with the variables | ||||
| @@ -64,11 +61,12 @@ var Translator = (function () { | ||||
| 				if (Object.prototype.toString.call(template) !== "[object String]") { | ||||
| 					return template; | ||||
| 				} | ||||
| 				let templateToUse = template; | ||||
| 				if (variables.fallback && !template.match(new RegExp("{.+}"))) { | ||||
| 					template = variables.fallback; | ||||
| 					templateToUse = variables.fallback; | ||||
| 				} | ||||
| 				return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) { | ||||
| 					return varName in variables ? variables[varName] : "{" + varName + "}"; | ||||
| 				return templateToUse.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) { | ||||
| 					return varName in variables ? variables[varName] : `{${varName}}`; | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| @@ -97,73 +95,49 @@ var Translator = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Load a translation file (json) and remember the data. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module to load the translation file for. | ||||
| 		 * @param {string} file Path of the file we want to load. | ||||
| 		 * @param {boolean} isFallback Flag to indicate fallback translations. | ||||
| 		 * @param {Function} callback Function called when done. | ||||
| 		 */ | ||||
| 		load: function (module, file, isFallback, callback) { | ||||
| 			if (!isFallback) { | ||||
| 				Log.log(module.name + " - Load translation: " + file); | ||||
| 			} else { | ||||
| 				Log.log(module.name + " - Load translation fallback: " + file); | ||||
| 		async load(module, file, isFallback) { | ||||
| 			Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`); | ||||
|  | ||||
| 			if (this.translationsFallback[module.name]) { | ||||
| 				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 { | ||||
| 				callback(); | ||||
| 			} | ||||
| 			const json = await loadJSON(module.file(file)); | ||||
| 			const property = isFallback ? "translationsFallback" : "translations"; | ||||
| 			this[property][module.name] = json; | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Load the core translations. | ||||
| 		 * | ||||
| 		 * @param {string} lang The language identifier of the core language. | ||||
| 		 */ | ||||
| 		loadCoreTranslations: function (lang) { | ||||
| 			var self = this; | ||||
|  | ||||
| 		loadCoreTranslations: async function (lang) { | ||||
| 			if (lang in translations) { | ||||
| 				Log.log("Loading core translation file: " + translations[lang]); | ||||
| 				loadJSON(translations[lang], function (translations) { | ||||
| 					self.coreTranslations = translations; | ||||
| 				}); | ||||
| 				Log.log(`Loading core translation file: ${translations[lang]}`); | ||||
| 				this.coreTranslations = await loadJSON(translations[lang]); | ||||
| 			} else { | ||||
| 				Log.log("Configured language not found in core translations."); | ||||
| 			} | ||||
|  | ||||
| 			self.loadCoreTranslationsFallback(); | ||||
| 			await this.loadCoreTranslationsFallback(); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Load the core translations fallback. | ||||
| 		 * Load the core translations' fallback. | ||||
| 		 * The first language defined in translations.js will be used. | ||||
| 		 */ | ||||
| 		loadCoreTranslationsFallback: function () { | ||||
| 			var self = this; | ||||
|  | ||||
| 			// The variable `first` will contain the first | ||||
| 			// defined translation after the following line. | ||||
| 			for (var first in translations) { | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 		loadCoreTranslationsFallback: async function () { | ||||
| 			let first = Object.keys(translations)[0]; | ||||
| 			if (first) { | ||||
| 				Log.log("Loading core translation fallback file: " + translations[first]); | ||||
| 				loadJSON(translations[first], function (translations) { | ||||
| 					self.coreTranslationsFallback = translations; | ||||
| 				}); | ||||
| 				Log.log(`Loading core translation fallback file: ${translations[first]}`); | ||||
| 				this.coreTranslationsFallback = await loadJSON(translations[first]); | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| })(); | ||||
|  | ||||
| window.Translator = Translator; | ||||
|   | ||||
							
								
								
									
										10
									
								
								js/utils.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								js/utils.js
									
									
									
									
									
								
							| @@ -1,12 +1,12 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Utils | ||||
|  * | ||||
|  * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var colors = require("colors/safe"); | ||||
| const colors = require("colors/safe"); | ||||
|  | ||||
| var Utils = { | ||||
| module.exports = { | ||||
| 	colors: { | ||||
| 		warn: colors.yellow, | ||||
| 		error: colors.red, | ||||
| @@ -14,7 +14,3 @@ var Utils = { | ||||
| 		pass: colors.green | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| if (typeof module !== "undefined") { | ||||
| 	module.exports = Utils; | ||||
| } | ||||
|   | ||||
| @@ -1,16 +1,19 @@ | ||||
| type ModuleProperties = { | ||||
|   defaults?: object, | ||||
|   start?(): void, | ||||
|   getHeader?(): string, | ||||
|   getTemplate?(): string, | ||||
|   getTemplateData?(): object, | ||||
|   notificationReceived?(notification: string, payload: any, sender: object): void, | ||||
|   socketNotificationReceived?(notification: string, payload: any): void, | ||||
|   suspend?(): void, | ||||
|   resume?(): void, | ||||
|   getDom?(): HTMLElement, | ||||
|   getStyles?(): string[], | ||||
|   [key: string]: any, | ||||
|   defaults?: object; | ||||
|   [key: string]: any; | ||||
|   start?(): void; | ||||
|   getScripts?(): string[]; | ||||
|   getStyles?(): string[]; | ||||
|   getTranslations?(): object; | ||||
|   getDom?(): HTMLElement; | ||||
|   getHeader?(): string; | ||||
|   getTemplate?(): string; | ||||
|   getTemplateData?(): object; | ||||
|   notificationReceived?(notification: string, payload: any, sender: object): void; | ||||
|   nunjucksEnvironment?(): void; | ||||
|   socketNotificationReceived?(notification: string, payload: any): void; | ||||
|   suspend?(): void; | ||||
|   resume?(): void; | ||||
| }; | ||||
|  | ||||
| export declare const Module: { | ||||
| @@ -18,14 +21,14 @@ export declare const Module: { | ||||
| }; | ||||
|  | ||||
| export declare const Log: { | ||||
|   info(message?: any, ...optionalParams: any[]): void, | ||||
|   log(message?: any, ...optionalParams: any[]): void, | ||||
|   error(message?: any, ...optionalParams: any[]): void, | ||||
|   warn(message?: any, ...optionalParams: any[]): void, | ||||
|   group(groupTitle?: string, ...optionalParams: any[]): void, | ||||
|   groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void, | ||||
|   groupEnd(): void, | ||||
|   time(timerName?: string): void, | ||||
|   timeEnd(timerName?: string): void, | ||||
|   timeStamp(timerName?: string): void, | ||||
| }; | ||||
|   info(message?: any, ...optionalParams: any[]): void; | ||||
|   log(message?: any, ...optionalParams: any[]): void; | ||||
|   error(message?: any, ...optionalParams: any[]): void; | ||||
|   warn(message?: any, ...optionalParams: any[]): void; | ||||
|   group(groupTitle?: string, ...optionalParams: any[]): void; | ||||
|   groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void; | ||||
|   groupEnd(): void; | ||||
|   time(timerName?: string): void; | ||||
|   timeEnd(timerName?: string): void; | ||||
|   timeStamp(timerName?: string): void; | ||||
| }; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # Module: Alert | ||||
|  | ||||
| The alert module is one of the default modules of the MagicMirror. This module displays notifications from other modules. | ||||
| The alert module is one of the default modules of the MagicMirror². This module displays notifications from other modules. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/alert.html). | ||||
|   | ||||
| @@ -1,167 +1,147 @@ | ||||
| /* global NotificationFx */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Module: alert | ||||
|  * | ||||
|  * By Paul-Vincent Roll https://paulvincentroll.com/ | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| Module.register("alert", { | ||||
| 	alerts: {}, | ||||
|  | ||||
| 	defaults: { | ||||
| 		// scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		effect: "slide", | ||||
| 		// scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		alert_effect: "jelly", | ||||
| 		//time a notification is displayed in seconds | ||||
| 		display_time: 3500, | ||||
| 		//Position | ||||
| 		effect: "slide", // scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		alert_effect: "jelly", // scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		display_time: 3500, // time a notification is displayed in seconds | ||||
| 		position: "center", | ||||
| 		//shown at startup | ||||
| 		welcome_message: false | ||||
| 		welcome_message: false // shown at startup | ||||
| 	}, | ||||
| 	getScripts: function () { | ||||
|  | ||||
| 	getScripts() { | ||||
| 		return ["notificationFx.js"]; | ||||
| 	}, | ||||
| 	getStyles: function () { | ||||
| 		return ["notificationFx.css", "font-awesome.css"]; | ||||
|  | ||||
| 	getStyles() { | ||||
| 		return ["font-awesome.css", this.file(`./styles/notificationFx.css`), this.file(`./styles/${this.config.position}.css`)]; | ||||
| 	}, | ||||
| 	// Define required translations. | ||||
| 	getTranslations: function () { | ||||
|  | ||||
| 	getTranslations() { | ||||
| 		return { | ||||
| 			en: "translations/en.json", | ||||
| 			bg: "translations/bg.json", | ||||
| 			da: "translations/da.json", | ||||
| 			de: "translations/de.json", | ||||
| 			nl: "translations/nl.json" | ||||
| 			en: "translations/en.json", | ||||
| 			es: "translations/es.json", | ||||
| 			fr: "translations/fr.json", | ||||
| 			hu: "translations/hu.json", | ||||
| 			nl: "translations/nl.json", | ||||
| 			ru: "translations/ru.json", | ||||
| 			th: "translations/th.json" | ||||
| 		}; | ||||
| 	}, | ||||
| 	show_notification: function (message) { | ||||
|  | ||||
| 	getTemplate(type) { | ||||
| 		return `templates/${type}.njk`; | ||||
| 	}, | ||||
|  | ||||
| 	async start() { | ||||
| 		Log.info(`Starting module: ${this.name}`); | ||||
|  | ||||
| 		if (this.config.effect === "slide") { | ||||
| 			this.config.effect = this.config.effect + "-" + this.config.position; | ||||
| 			this.config.effect = `${this.config.effect}-${this.config.position}`; | ||||
| 		} | ||||
| 		let msg = ""; | ||||
| 		if (message.title) { | ||||
| 			msg += "<span class='thin dimmed medium'>" + message.title + "</span>"; | ||||
|  | ||||
| 		if (this.config.welcome_message) { | ||||
| 			const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message; | ||||
| 			await this.showNotification({ title: this.translate("sysTitle"), message }); | ||||
| 		} | ||||
| 		if (message.message) { | ||||
| 			if (msg !== "") { | ||||
| 				msg += "<br />"; | ||||
| 	}, | ||||
|  | ||||
| 	notificationReceived(notification, payload, sender) { | ||||
| 		if (notification === "SHOW_ALERT") { | ||||
| 			if (payload.type === "notification") { | ||||
| 				this.showNotification(payload); | ||||
| 			} else { | ||||
| 				this.showAlert(payload, sender); | ||||
| 			} | ||||
| 			msg += "<span class='light bright small'>" + message.message + "</span>"; | ||||
| 		} else if (notification === "HIDE_ALERT") { | ||||
| 			this.hideAlert(sender); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	async showNotification(notification) { | ||||
| 		const message = await this.renderMessage(notification.templateName || "notification", notification); | ||||
|  | ||||
| 		new NotificationFx({ | ||||
| 			message: msg, | ||||
| 			message, | ||||
| 			layout: "growl", | ||||
| 			effect: this.config.effect, | ||||
| 			ttl: message.timer !== undefined ? message.timer : this.config.display_time | ||||
| 			ttl: notification.timer || this.config.display_time | ||||
| 		}).show(); | ||||
| 	}, | ||||
| 	show_alert: function (params, sender) { | ||||
| 		let image = ""; | ||||
| 		//Set standard params if not provided by module | ||||
| 		if (typeof params.timer === "undefined") { | ||||
| 			params.timer = null; | ||||
| 		} | ||||
| 		if (typeof params.imageHeight === "undefined") { | ||||
| 			params.imageHeight = "80px"; | ||||
| 		} | ||||
| 		if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") { | ||||
| 			params.imageUrl = null; | ||||
| 		} else if (typeof params.imageFA === "undefined") { | ||||
| 			image = "<img src='" + params.imageUrl.toString() + "' height='" + params.imageHeight.toString() + "' style='margin-bottom: 10px;'/><br />"; | ||||
| 		} else if (typeof params.imageUrl === "undefined") { | ||||
| 			image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + params.imageHeight.toString() + ";'/></span><br />"; | ||||
| 		} | ||||
| 		//Create overlay | ||||
| 		const overlay = document.createElement("div"); | ||||
| 		overlay.id = "overlay"; | ||||
| 		overlay.innerHTML += '<div class="black_overlay"></div>'; | ||||
| 		document.body.insertBefore(overlay, document.body.firstChild); | ||||
|  | ||||
| 		//If module already has an open alert close it | ||||
| 	async showAlert(alert, sender) { | ||||
| 		// If module already has an open alert close it | ||||
| 		if (this.alerts[sender.name]) { | ||||
| 			this.hide_alert(sender); | ||||
| 			this.hideAlert(sender, false); | ||||
| 		} | ||||
|  | ||||
| 		//Display title and message only if they are provided in notification parameters | ||||
| 		let message = ""; | ||||
| 		if (params.title) { | ||||
| 			message += "<span class='light dimmed medium'>" + params.title + "</span>"; | ||||
| 		} | ||||
| 		if (params.message) { | ||||
| 			if (message !== "") { | ||||
| 				message += "<br />"; | ||||
| 			} | ||||
|  | ||||
| 			message += "<span class='thin bright small'>" + params.message + "</span>"; | ||||
| 		// Add overlay | ||||
| 		if (!Object.keys(this.alerts).length) { | ||||
| 			this.toggleBlur(true); | ||||
| 		} | ||||
|  | ||||
| 		//Store alert in this.alerts | ||||
| 		const message = await this.renderMessage(alert.templateName || "alert", alert); | ||||
|  | ||||
| 		// Store alert in this.alerts | ||||
| 		this.alerts[sender.name] = new NotificationFx({ | ||||
| 			message: image + message, | ||||
| 			message, | ||||
| 			effect: this.config.alert_effect, | ||||
| 			ttl: params.timer, | ||||
| 			onClose: () => this.hide_alert(sender), | ||||
| 			ttl: alert.timer, | ||||
| 			onClose: () => this.hideAlert(sender), | ||||
| 			al_no: "ns-alert" | ||||
| 		}); | ||||
|  | ||||
| 		//Show alert | ||||
| 		// Show alert | ||||
| 		this.alerts[sender.name].show(); | ||||
|  | ||||
| 		//Add timer to dismiss alert and overlay | ||||
| 		if (params.timer) { | ||||
| 		// Add timer to dismiss alert and overlay | ||||
| 		if (alert.timer) { | ||||
| 			setTimeout(() => { | ||||
| 				this.hide_alert(sender); | ||||
| 			}, params.timer); | ||||
| 				this.hideAlert(sender); | ||||
| 			}, alert.timer); | ||||
| 		} | ||||
| 	}, | ||||
| 	hide_alert: function (sender) { | ||||
| 		//Dismiss alert and remove from this.alerts | ||||
|  | ||||
| 	hideAlert(sender, close = true) { | ||||
| 		// Dismiss alert and remove from this.alerts | ||||
| 		if (this.alerts[sender.name]) { | ||||
| 			this.alerts[sender.name].dismiss(); | ||||
| 			this.alerts[sender.name] = null; | ||||
| 			//Remove overlay | ||||
| 			const overlay = document.getElementById("overlay"); | ||||
| 			overlay.parentNode.removeChild(overlay); | ||||
| 		} | ||||
| 	}, | ||||
| 	setPosition: function (pos) { | ||||
| 		//Add css to body depending on the set position for notifications | ||||
| 		const sheet = document.createElement("style"); | ||||
| 		if (pos === "center") { | ||||
| 			sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}"; | ||||
| 		} | ||||
| 		if (pos === "right") { | ||||
| 			sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}"; | ||||
| 		} | ||||
| 		if (pos === "left") { | ||||
| 			sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}"; | ||||
| 		} | ||||
| 		document.body.appendChild(sheet); | ||||
| 	}, | ||||
| 	notificationReceived: function (notification, payload, sender) { | ||||
| 		if (notification === "SHOW_ALERT") { | ||||
| 			if (typeof payload.type === "undefined") { | ||||
| 				payload.type = "alert"; | ||||
| 			} | ||||
| 			if (payload.type === "alert") { | ||||
| 				this.show_alert(payload, sender); | ||||
| 			} else if (payload.type === "notification") { | ||||
| 				this.show_notification(payload); | ||||
| 			} | ||||
| 		} else if (notification === "HIDE_ALERT") { | ||||
| 			this.hide_alert(sender); | ||||
| 		} | ||||
| 	}, | ||||
| 	start: function () { | ||||
| 		this.alerts = {}; | ||||
| 		this.setPosition(this.config.position); | ||||
| 		if (this.config.welcome_message) { | ||||
| 			if (this.config.welcome_message === true) { | ||||
| 				this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") }); | ||||
| 			} else { | ||||
| 				this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message }); | ||||
| 			this.alerts[sender.name].dismiss(close); | ||||
| 			delete this.alerts[sender.name]; | ||||
| 			// Remove overlay | ||||
| 			if (!Object.keys(this.alerts).length) { | ||||
| 				this.toggleBlur(false); | ||||
| 			} | ||||
| 		} | ||||
| 		Log.info("Starting module: " + this.name); | ||||
| 	}, | ||||
|  | ||||
| 	renderMessage(type, data) { | ||||
| 		return new Promise((resolve) => { | ||||
| 			this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) { | ||||
| 				if (err) { | ||||
| 					Log.error("Failed to render alert", err); | ||||
| 				} | ||||
|  | ||||
| 				resolve(res); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	toggleBlur(add = false) { | ||||
| 		const method = add ? "add" : "remove"; | ||||
| 		const modules = document.querySelectorAll(".module"); | ||||
| 		for (const module of modules) { | ||||
| 			module.classList[method]("alert-blur"); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -9,11 +9,11 @@ | ||||
|  * | ||||
|  * Copyright 2014, Codrops | ||||
|  * https://tympanus.net/codrops/ | ||||
|  * @param {object} window The window object | ||||
|  */ | ||||
| (function (window) { | ||||
| 	/** | ||||
| 	 * Extend one object with another one | ||||
| 	 * | ||||
| 	 * @param {object} a The object to extend | ||||
| 	 * @param {object} b The object which extends the other, overwrites existing keys | ||||
| 	 * @returns {object} The merged object | ||||
| @@ -29,7 +29,6 @@ | ||||
|  | ||||
| 	/** | ||||
| 	 * NotificationFx constructor | ||||
| 	 * | ||||
| 	 * @param {object} options The configuration options | ||||
| 	 * @class | ||||
| 	 */ | ||||
| @@ -78,7 +77,7 @@ | ||||
| 	NotificationFx.prototype._init = function () { | ||||
| 		// create HTML structure | ||||
| 		this.ntf = document.createElement("div"); | ||||
| 		this.ntf.className = this.options.al_no + " ns-" + this.options.layout + " ns-effect-" + this.options.effect + " ns-type-" + this.options.type; | ||||
| 		this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`; | ||||
| 		let strinner = '<div class="ns-box-inner">'; | ||||
| 		strinner += this.options.message; | ||||
| 		strinner += "</div>"; | ||||
| @@ -122,8 +121,9 @@ | ||||
|  | ||||
| 	/** | ||||
| 	 * Dismiss the notification | ||||
| 	 * @param {boolean} [close] call the onClose callback at the end | ||||
| 	 */ | ||||
| 	NotificationFx.prototype.dismiss = function () { | ||||
| 	NotificationFx.prototype.dismiss = function (close = true) { | ||||
| 		this.active = false; | ||||
| 		clearTimeout(this.dismissttl); | ||||
| 		this.ntf.classList.remove("ns-show"); | ||||
| @@ -131,7 +131,7 @@ | ||||
| 			this.ntf.classList.add("ns-hide"); | ||||
|  | ||||
| 			// callback | ||||
| 			this.options.onClose(); | ||||
| 			if (close) this.options.onClose(); | ||||
| 		}, 25); | ||||
|  | ||||
| 		// after animation ends remove ntf from the DOM | ||||
|   | ||||
							
								
								
									
										5
									
								
								modules/default/alert/styles/center.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								modules/default/alert/styles/center.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| .ns-box { | ||||
|   margin-left: auto; | ||||
|   margin-right: auto; | ||||
|   text-align: center; | ||||
| } | ||||
							
								
								
									
										4
									
								
								modules/default/alert/styles/left.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/styles/left.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| .ns-box { | ||||
|   margin-right: auto; | ||||
|   text-align: left; | ||||
| } | ||||
| @@ -1,12 +1,11 @@ | ||||
| /* Based on work by https://tympanus.net/codrops/licensing/ */ | ||||
| 
 | ||||
| .ns-box { | ||||
|   background-color: rgba(0, 0, 0, 0.93); | ||||
|   background-color: rgb(0 0 0 / 93%); | ||||
|   padding: 17px; | ||||
|   line-height: 1.4; | ||||
|   margin-bottom: 10px; | ||||
|   z-index: 1; | ||||
|   color: black; | ||||
|   font-size: 70%; | ||||
|   position: relative; | ||||
|   display: table; | ||||
| @@ -15,17 +14,17 @@ | ||||
|   border-width: 1px; | ||||
|   border-radius: 5px; | ||||
|   border-style: solid; | ||||
|   border-color: #666; | ||||
|   border-color: var(--color-text-dimmed); | ||||
| } | ||||
| 
 | ||||
| .ns-alert { | ||||
|   border-style: solid; | ||||
|   border-color: #fff; | ||||
|   border-color: var(--color-text-bright); | ||||
|   padding: 17px; | ||||
|   line-height: 1.4; | ||||
|   margin-bottom: 10px; | ||||
|   z-index: 3; | ||||
|   color: white; | ||||
|   color: var(--color-text-bright); | ||||
|   font-size: 70%; | ||||
|   position: fixed; | ||||
|   text-align: center; | ||||
| @@ -40,12 +39,8 @@ | ||||
|   border-radius: 20px; | ||||
| } | ||||
| 
 | ||||
| .black_overlay { | ||||
|   position: fixed; | ||||
|   z-index: 2; | ||||
|   background-color: rgba(0, 0, 0, 0.93); | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| .alert-blur { | ||||
|   filter: blur(2px) brightness(50%); | ||||
| } | ||||
| 
 | ||||
| [class^="ns-effect-"].ns-growl.ns-hide, | ||||
| @@ -60,15 +55,15 @@ | ||||
| 
 | ||||
| .ns-effect-flip.ns-show, | ||||
| .ns-effect-flip.ns-hide { | ||||
|   animation-name: animFlipFront; | ||||
|   animation-name: anim-flip-front; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| .ns-effect-flip.ns-hide { | ||||
|   animation-name: animFlipBack; | ||||
|   animation-name: anim-flip-back; | ||||
| } | ||||
| 
 | ||||
| @keyframes animFlipFront { | ||||
| @keyframes anim-flip-front { | ||||
|   0% { | ||||
|     transform: perspective(1000px) rotate3d(1, 0, 0, -90deg); | ||||
|   } | ||||
| @@ -78,7 +73,7 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes animFlipBack { | ||||
| @keyframes anim-flip-back { | ||||
|   0% { | ||||
|     transform: perspective(1000px) rotate3d(1, 0, 0, 90deg); | ||||
|   } | ||||
| @@ -90,11 +85,11 @@ | ||||
| 
 | ||||
| .ns-effect-bouncyflip.ns-show, | ||||
| .ns-effect-bouncyflip.ns-hide { | ||||
|   animation-name: flipInX; | ||||
|   animation-name: flip-in-x; | ||||
|   animation-duration: 0.8s; | ||||
| } | ||||
| 
 | ||||
| @keyframes flipInX { | ||||
| @keyframes flip-in-x { | ||||
|   0% { | ||||
|     transform: perspective(400px) rotate3d(1, 0, 0, -90deg); | ||||
|     transition-timing-function: ease-in; | ||||
| @@ -122,11 +117,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-bouncyflip.ns-hide { | ||||
|   animation-name: flipInXSimple; | ||||
|   animation-name: flip-in-x-simple; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| @keyframes flipInXSimple { | ||||
| @keyframes flip-in-x-simple { | ||||
|   0% { | ||||
|     transform: perspective(400px) rotate3d(1, 0, 0, -90deg); | ||||
|     transition-timing-function: ease-in; | ||||
| @@ -146,11 +141,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-show { | ||||
|   animation-name: animLoad; | ||||
|   animation-name: anim-load; | ||||
|   animation-duration: 1s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animLoad { | ||||
| @keyframes anim-load { | ||||
|   0% { | ||||
|     opacity: 1; | ||||
|     transform: scale3d(0, 0.3, 1); | ||||
| @@ -163,7 +158,7 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-hide { | ||||
|   animation-name: animFade; | ||||
|   animation-name: anim-fade; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| @@ -175,15 +170,15 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-show .ns-close { | ||||
|   animation-name: animFade; | ||||
|   animation-name: anim-fade; | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-show .ns-box-inner { | ||||
|   animation-name: animFadeMove; | ||||
|   animation-name: anim-fade-move; | ||||
|   animation-timing-function: ease-out; | ||||
| } | ||||
| 
 | ||||
| @keyframes animFadeMove { | ||||
| @keyframes anim-fade-move { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     transform: translate3d(0, 10px, 0); | ||||
| @@ -195,7 +190,7 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes animFade { | ||||
| @keyframes anim-fade { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
| @@ -207,11 +202,11 @@ | ||||
| 
 | ||||
| .ns-effect-scale.ns-show, | ||||
| .ns-effect-scale.ns-hide { | ||||
|   animation-name: animScale; | ||||
|   animation-name: anim-scale; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animScale { | ||||
| @keyframes anim-scale { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1); | ||||
| @@ -224,168 +219,169 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-jelly.ns-show { | ||||
|   animation-name: animJelly; | ||||
|   animation-name: anim-jelly; | ||||
|   animation-duration: 1s; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| 
 | ||||
| .ns-effect-jelly.ns-hide { | ||||
|   animation-name: animFade; | ||||
|   animation-name: anim-fade; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animFade { | ||||
| @keyframes anim-fade { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     opacity: 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes animJelly { | ||||
| @keyframes anim-jelly { | ||||
|   0% { | ||||
|     transform: matrix3d(0.7, 0, 0, 0, 0, 0.7, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   2.083333% { | ||||
|     transform: matrix3d(0.75266, 0, 0, 0, 0, 0.76342, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.7527, 0, 0, 0, 0, 0.7634, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   4.166667% { | ||||
|     transform: matrix3d(0.81071, 0, 0, 0, 0, 0.84545, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.8107, 0, 0, 0, 0, 0.8454, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   6.25% { | ||||
|     transform: matrix3d(0.86808, 0, 0, 0, 0, 0.9286, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.8681, 0, 0, 0, 0, 0.929, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   8.333333% { | ||||
|     transform: matrix3d(0.92038, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   10.416667% { | ||||
|     transform: matrix3d(0.96482, 0, 0, 0, 0, 1.05202, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9648, 0, 0, 0, 0, 1.052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   12.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.08204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.082, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   14.583333% { | ||||
|     transform: matrix3d(1.02563, 0, 0, 0, 0, 1.09149, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0256, 0, 0, 0, 0, 1.0915, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   16.666667% { | ||||
|     transform: matrix3d(1.04227, 0, 0, 0, 0, 1.08453, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0423, 0, 0, 0, 0, 1.0845, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   18.75% { | ||||
|     transform: matrix3d(1.05102, 0, 0, 0, 0, 1.06666, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.051, 0, 0, 0, 0, 1.0667, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   20.833333% { | ||||
|     transform: matrix3d(1.05334, 0, 0, 0, 0, 1.04355, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0533, 0, 0, 0, 0, 1.0436, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   22.916667% { | ||||
|     transform: matrix3d(1.05078, 0, 0, 0, 0, 1.02012, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0508, 0, 0, 0, 0, 1.0201, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   25% { | ||||
|     transform: matrix3d(1.04487, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0449, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   27.083333% { | ||||
|     transform: matrix3d(1.03699, 0, 0, 0, 0, 0.98534, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.037, 0, 0, 0, 0, 0.9853, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   29.166667% { | ||||
|     transform: matrix3d(1.02831, 0, 0, 0, 0, 0.97688, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0283, 0, 0, 0, 0, 0.9769, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   31.25% { | ||||
|     transform: matrix3d(1.01973, 0, 0, 0, 0, 0.97422, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0197, 0, 0, 0, 0, 0.9742, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   33.333333% { | ||||
|     transform: matrix3d(1.01191, 0, 0, 0, 0, 0.97618, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0119, 0, 0, 0, 0, 0.9762, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   35.416667% { | ||||
|     transform: matrix3d(1.00526, 0, 0, 0, 0, 0.98122, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0053, 0, 0, 0, 0, 0.9812, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   37.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.98773, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.9877, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   39.583333% { | ||||
|     transform: matrix3d(0.99617, 0, 0, 0, 0, 0.99433, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9962, 0, 0, 0, 0, 0.9943, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   41.666667% { | ||||
|     transform: matrix3d(0.99368, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9937, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   43.75% { | ||||
|     transform: matrix3d(0.99237, 0, 0, 0, 0, 1.00413, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0041, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   45.833333% { | ||||
|     transform: matrix3d(0.99202, 0, 0, 0, 0, 1.00651, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.992, 0, 0, 0, 0, 1.0065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   47.916667% { | ||||
|     transform: matrix3d(0.99241, 0, 0, 0, 0, 1.00726, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0073, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   50% { | ||||
|     transform: matrix3d(0.99329, 0, 0, 0, 0, 1.00671, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9933, 0, 0, 0, 0, 1.0067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   52.083333% { | ||||
|     transform: matrix3d(0.99447, 0, 0, 0, 0, 1.00529, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9945, 0, 0, 0, 0, 1.0053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   54.166667% { | ||||
|     transform: matrix3d(0.99577, 0, 0, 0, 0, 1.00346, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9958, 0, 0, 0, 0, 1.0035, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   56.25% { | ||||
|     transform: matrix3d(0.99705, 0, 0, 0, 0, 1.0016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.997, 0, 0, 0, 0, 1.002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   58.333333% { | ||||
|     transform: matrix3d(0.99822, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   60.416667% { | ||||
|     transform: matrix3d(0.99921, 0, 0, 0, 0, 0.99884, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9992, 0, 0, 0, 0, 0.9989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   62.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.99816, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   64.583333% { | ||||
|     transform: matrix3d(1.00057, 0, 0, 0, 0, 0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0006, 0, 0, 0, 0, 0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   66.666667% { | ||||
|     transform: matrix3d(1.00095, 0, 0, 0, 0, 0.99811, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.001, 0, 0, 0, 0, 0.9981, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   68.75% { | ||||
|     transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   70.833333% { | ||||
|     transform: matrix3d(1.00119, 0, 0, 0, 0, 0.99903, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0012, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   72.916667% { | ||||
|     transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99955, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   75% { | ||||
| @@ -393,47 +389,47 @@ | ||||
|   } | ||||
| 
 | ||||
|   77.083333% { | ||||
|     transform: matrix3d(1.00083, 0, 0, 0, 0, 1.00033, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0008, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   79.166667% { | ||||
|     transform: matrix3d(1.00063, 0, 0, 0, 0, 1.00052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0006, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   81.25% { | ||||
|     transform: matrix3d(1.00044, 0, 0, 0, 0, 1.00058, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0004, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   83.333333% { | ||||
|     transform: matrix3d(1.00027, 0, 0, 0, 0, 1.00053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0003, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   85.416667% { | ||||
|     transform: matrix3d(1.00012, 0, 0, 0, 0, 1.00042, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0001, 0, 0, 0, 0, 1.0004, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   87.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.00027, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   89.583333% { | ||||
|     transform: matrix3d(0.99991, 0, 0, 0, 0, 1.00013, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9999, 0, 0, 0, 0, 1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   91.666667% { | ||||
|     transform: matrix3d(0.99986, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   93.75% { | ||||
|     transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   95.833333% { | ||||
|     transform: matrix3d(0.99982, 0, 0, 0, 0, 0.99985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   97.916667% { | ||||
|     transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99984, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
| @@ -442,162 +438,162 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-left.ns-show { | ||||
|   animation-name: animSlideElasticLeft; | ||||
|   animation-name: anim-slide-elastic-left; | ||||
|   animation-duration: 1s; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideElasticLeft { | ||||
| @keyframes anim-slide-elastic-left { | ||||
|   0% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1000, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   1.666667% { | ||||
|     transform: matrix3d(1.92933, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.26805, 0, 0, 1); | ||||
|     transform: matrix3d(1.9293, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.2681, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   3.333333% { | ||||
|     transform: matrix3d(1.96989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.82545, 0, 0, 1); | ||||
|     transform: matrix3d(1.9699, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.8255, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   5% { | ||||
|     transform: matrix3d(1.70901, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.26115, 0, 0, 1); | ||||
|     transform: matrix3d(1.709, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.2612, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   6.666667% { | ||||
|     transform: matrix3d(1.4235, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.3238, 0, 0, 1); | ||||
|     transform: matrix3d(1.424, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.324, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   8.333333% { | ||||
|     transform: matrix3d(1.21065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.29848, 0, 0, 1); | ||||
|     transform: matrix3d(1.2107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.2985, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   10% { | ||||
|     transform: matrix3d(1.08167, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.59273, 0, 0, 1); | ||||
|     transform: matrix3d(1.0817, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.5927, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   11.666667% { | ||||
|     transform: matrix3d(1.0165, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.72371, 0, 0, 1); | ||||
|     transform: matrix3d(1.017, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.7237, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   13.333333% { | ||||
|     transform: matrix3d(0.99057, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.12794, 0, 0, 1); | ||||
|     transform: matrix3d(0.9906, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.1279, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   15% { | ||||
|     transform: matrix3d(0.98478, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.86339, 0, 0, 1); | ||||
|     transform: matrix3d(0.9848, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.8634, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   16.666667% { | ||||
|     transform: matrix3d(0.98719, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.40503, 0, 0, 1); | ||||
|     transform: matrix3d(0.9872, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.405, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   18.333333% { | ||||
|     transform: matrix3d(0.9916, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.75275, 0, 0, 1); | ||||
|     transform: matrix3d(0.992, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.7528, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   20% { | ||||
|     transform: matrix3d(0.99541, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.10141, 0, 0, 1); | ||||
|     transform: matrix3d(0.9954, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.1014, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   21.666667% { | ||||
|     transform: matrix3d(0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.98271, 0, 0, 1); | ||||
|     transform: matrix3d(0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.9827, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   23.333333% { | ||||
|     transform: matrix3d(0.99936, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.40752, 0, 0, 1); | ||||
|     transform: matrix3d(0.9994, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.4075, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   25% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.99558, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.9956, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   26.666667% { | ||||
|     transform: matrix3d(1.00021, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.08575, 0, 0, 1); | ||||
|     transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.0858, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   28.333333% { | ||||
|     transform: matrix3d(1.00022, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.82507, 0, 0, 1); | ||||
|     transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.8251, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   30% { | ||||
|     transform: matrix3d(1.00016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.23737, 0, 0, 1); | ||||
|     transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.2374, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   31.666667% { | ||||
|     transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.27389, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.2739, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   33.333333% { | ||||
|     transform: matrix3d(1.00005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.84893, 0, 0, 1); | ||||
|     transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.8489, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   35% { | ||||
|     transform: matrix3d(1.00002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.86364, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.8636, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   36.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.22079, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.2208, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   38.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16687, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1669, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   40% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.37284, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3728, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   41.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.45594, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4559, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   43.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.46116, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4612, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   45% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4214, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.421, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   46.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.35963, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3596, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   48.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.29103, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.291, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   50% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.22487, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.2249, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   51.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16624, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1662, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   53.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.11734, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1173, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   55% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.07854, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0785, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   56.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.04909, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0491, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   58.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.02773, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0277, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   60% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.01295, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.013, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   61.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00331, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0033, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   63.333333% { | ||||
| @@ -605,67 +601,67 @@ | ||||
|   } | ||||
| 
 | ||||
|   65% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00559, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0056, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   66.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00684, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0068, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   68.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00692, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0069, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   70% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00632, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0063, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   71.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00539, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0054, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   73.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00436, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0044, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   75% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00337, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0034, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   76.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00249, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0025, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   78.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00176, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0018, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   80% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00118, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0012, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   81.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00074, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0007, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   83.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00042, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0004, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   85% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00019, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0002, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   86.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00005, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   88.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00004, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   90% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   91.666667% { | ||||
| @@ -677,15 +673,15 @@ | ||||
|   } | ||||
| 
 | ||||
|   95% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00009, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   96.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   98.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00007, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
| @@ -694,11 +690,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-left.ns-hide { | ||||
|   animation-name: animSlideLeft; | ||||
|   animation-name: anim-slide-left; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideLeft { | ||||
| @keyframes anim-slide-left { | ||||
|   0% { | ||||
|     transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0); | ||||
|   } | ||||
| @@ -709,10 +705,10 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-right.ns-show { | ||||
|   animation: animSlideElasticRight 2000ms linear both; | ||||
|   animation: anim-slide-elastic-right 2000ms linear both; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideElasticRight { | ||||
| @keyframes anim-slide-elastic-right { | ||||
|   0% { | ||||
|     transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1); | ||||
|   } | ||||
| @@ -791,11 +787,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-right.ns-hide { | ||||
|   animation-name: animSlideRight; | ||||
|   animation-name: anim-slide-right; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideRight { | ||||
| @keyframes anim-slide-right { | ||||
|   0% { | ||||
|     transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0); | ||||
|   } | ||||
| @@ -806,10 +802,10 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-center.ns-show { | ||||
|   animation: animSlideElasticCenter 2000ms linear both; | ||||
|   animation: anim-slide-elastic-center 2000ms linear both; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideElasticCenter { | ||||
| @keyframes anim-slide-elastic-center { | ||||
|   0% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1); | ||||
|   } | ||||
| @@ -888,11 +884,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-center.ns-hide { | ||||
|   animation-name: animSlideCenter; | ||||
|   animation-name: anim-slide-center; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideCenter { | ||||
| @keyframes anim-slide-center { | ||||
|   0% { | ||||
|     transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0); | ||||
|   } | ||||
| @@ -904,11 +900,11 @@ | ||||
| 
 | ||||
| .ns-effect-genie.ns-show, | ||||
| .ns-effect-genie.ns-hide { | ||||
|   animation-name: animGenie; | ||||
|   animation-name: anim-genie; | ||||
|   animation-duration: 0.4s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animGenie { | ||||
| @keyframes anim-genie { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1); | ||||
							
								
								
									
										4
									
								
								modules/default/alert/styles/right.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/styles/right.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| .ns-box { | ||||
|   margin-left: auto; | ||||
|   text-align: right; | ||||
| } | ||||
							
								
								
									
										20
									
								
								modules/default/alert/templates/alert.njk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								modules/default/alert/templates/alert.njk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| {% if imageUrl or imageFA %} | ||||
|     {% set imageHeight = imageHeight if imageHeight else "80px" %} | ||||
|     {% if imageUrl %} | ||||
|         <img src="{{ imageUrl }}" | ||||
|              height="{{ imageHeight }}" | ||||
|              style="margin-bottom: 10px" /> | ||||
|     {% else %} | ||||
|         <span class="bright fas fa-{{ imageFA }}" | ||||
|               style="margin-bottom: 10px; | ||||
|                      font-size: {{ imageHeight }}"></span> | ||||
|     {% endif %} | ||||
|     <br /> | ||||
| {% endif %} | ||||
| {% if title %} | ||||
|     <span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span> | ||||
| {% endif %} | ||||
| {% if message %} | ||||
|     {% if title %}<br />{% endif %} | ||||
|     <span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span> | ||||
| {% endif %} | ||||
							
								
								
									
										7
									
								
								modules/default/alert/templates/notification.njk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								modules/default/alert/templates/notification.njk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| {% if title %} | ||||
|     <span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span> | ||||
| {% endif %} | ||||
| {% if message %} | ||||
|     {% if title %}<br />{% endif %} | ||||
|     <span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span> | ||||
| {% endif %} | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror нотификация", | ||||
| 	"sysTitle": "MagicMirror² нотификация", | ||||
| 	"welcome": "Добре дошли, стартирането беше успешно" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notifikation", | ||||
| 	"sysTitle": "MagicMirror² Notifikation", | ||||
| 	"welcome": "Velkommen, modulet er succesfuldt startet!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Benachrichtigung", | ||||
| 	"sysTitle": "MagicMirror² Benachrichtigung", | ||||
| 	"welcome": "Willkommen, Start war erfolgreich!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notification", | ||||
| 	"sysTitle": "MagicMirror² Notification", | ||||
| 	"welcome": "Welcome, start was successful!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notificaciones", | ||||
| 	"sysTitle": "MagicMirror² Notificaciones", | ||||
| 	"welcome": "Bienvenido, ¡se iniciado correctamente!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notification", | ||||
| 	"sysTitle": "MagicMirror² Notification", | ||||
| 	"welcome": "Bienvenue, le démarrage a été un succès!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror értesítés", | ||||
| 	"sysTitle": "MagicMirror² értesítés", | ||||
| 	"welcome": "Üdvözöljük, indulás sikeres!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notificatie", | ||||
| 	"sysTitle": "MagicMirror² Notificatie", | ||||
| 	"welcome": "Welkom, Succesvol gestart!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Уведомление", | ||||
| 	"sysTitle": "MagicMirror² Уведомление", | ||||
| 	"welcome": "Добро пожаловать, старт был успешным!" | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								modules/default/alert/translations/th.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/translations/th.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "การแจ้งเตือน MagicMirror²", | ||||
| 	"welcome": "ยินดีต้อนรับ การเริ่มต้นสำเร็จแล้ว!" | ||||
| } | ||||
							
								
								
									
										2
									
								
								modules/default/calendar/README.md
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										2
									
								
								modules/default/calendar/README.md
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,6 +1,6 @@ | ||||
| # Module: Calendar | ||||
|  | ||||
| The `calendar` module is one of the default modules of the MagicMirror. | ||||
| The `calendar` module is one of the default modules of the MagicMirror². | ||||
| This module displays events from a public .ical calendar. It can combine multiple calendars. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/calendar.html). | ||||
|   | ||||
| @@ -1,18 +1,20 @@ | ||||
| .calendar .symbol { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   justify-content: flex-end; | ||||
|   padding-left: 0; | ||||
|   padding-right: 10px; | ||||
|   font-size: 80%; | ||||
|   vertical-align: top; | ||||
|   font-size: var(--font-size-small); | ||||
| } | ||||
|  | ||||
| .calendar .symbol span { | ||||
|   display: inline-block; | ||||
|   transform: translate(0, 2px); | ||||
|   padding-top: 4px; | ||||
| } | ||||
|  | ||||
| .calendar .title { | ||||
|   padding-left: 0; | ||||
|   padding-right: 0; | ||||
|   vertical-align: top; | ||||
| } | ||||
|  | ||||
| .calendar .time { | ||||
|   | ||||
							
								
								
									
										694
									
								
								modules/default/calendar/calendar.js
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										694
									
								
								modules/default/calendar/calendar.js
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,20 +1,15 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Node Helper: Calendar - CalendarFetcher | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const Log = require("../../../js/logger.js"); | ||||
| const ical = require("node-ical"); | ||||
| const request = require("request"); | ||||
|  | ||||
| /** | ||||
|  * Moment date | ||||
|  * | ||||
|  * @external Moment | ||||
|  * @see {@link http://momentjs.com} | ||||
|  */ | ||||
| const moment = require("moment"); | ||||
| const https = require("https"); | ||||
| const ical = require("node-ical"); | ||||
| const Log = require("logger"); | ||||
| const NodeHelper = require("node_helper"); | ||||
| const CalendarFetcherUtils = require("./calendarfetcherutils"); | ||||
|  | ||||
| /** | ||||
|  * | ||||
| @@ -25,11 +20,10 @@ const moment = require("moment"); | ||||
|  * @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) { | ||||
| 	const self = this; | ||||
|  | ||||
| const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) { | ||||
| 	let reloadTimer = null; | ||||
| 	let events = []; | ||||
|  | ||||
| @@ -39,492 +33,55 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
| 	/** | ||||
| 	 * Initiates calendar fetch. | ||||
| 	 */ | ||||
| 	const fetchCalendar = function () { | ||||
| 	const fetchCalendar = () => { | ||||
| 		clearTimeout(reloadTimer); | ||||
| 		reloadTimer = null; | ||||
|  | ||||
| 		const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); | ||||
| 		const opts = { | ||||
| 			headers: { | ||||
| 				"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)" | ||||
| 			}, | ||||
| 			gzip: true | ||||
| 		let httpsAgent = null; | ||||
| 		let headers = { | ||||
| 			"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}` | ||||
| 		}; | ||||
|  | ||||
| 		if (selfSignedCert) { | ||||
| 			httpsAgent = new https.Agent({ | ||||
| 				rejectUnauthorized: false | ||||
| 			}); | ||||
| 		} | ||||
| 		if (auth) { | ||||
| 			if (auth.method === "bearer") { | ||||
| 				opts.auth = { | ||||
| 					bearer: auth.pass | ||||
| 				}; | ||||
| 				headers.Authorization = `Bearer ${auth.pass}`; | ||||
| 			} else { | ||||
| 				opts.auth = { | ||||
| 					user: auth.user, | ||||
| 					pass: auth.pass, | ||||
| 					sendImmediately: auth.method !== "digest" | ||||
| 				}; | ||||
| 				headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		request(url, opts, function (err, r, requestData) { | ||||
| 			if (err) { | ||||
| 				fetchFailedCallback(self, err); | ||||
| 				scheduleTimer(); | ||||
| 				return; | ||||
| 			} else if (r.statusCode !== 200) { | ||||
| 				fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage); | ||||
| 				scheduleTimer(); | ||||
| 				return; | ||||
| 			} | ||||
| 		fetch(url, { headers: headers, agent: httpsAgent }) | ||||
| 			.then(NodeHelper.checkFetchStatus) | ||||
| 			.then((response) => response.text()) | ||||
| 			.then((responseData) => { | ||||
| 				let data = []; | ||||
|  | ||||
| 			let data = []; | ||||
|  | ||||
| 			try { | ||||
| 				data = ical.parseICS(requestData); | ||||
| 			} catch (error) { | ||||
| 				fetchFailedCallback(self, error.message); | ||||
| 				scheduleTimer(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			Log.debug(" parsed data=" + JSON.stringify(data)); | ||||
|  | ||||
| 			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 isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time])); | ||||
| 			}; | ||||
| 			Log.debug("there are " + Object.entries(data).length + " calendar entries"); | ||||
| 			Object.entries(data).forEach(([key, event]) => { | ||||
| 				const now = new Date(); | ||||
| 				const today = moment().startOf("day").toDate(); | ||||
| 				const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat. | ||||
| 				let past = today; | ||||
| 				Log.debug("have entries "); | ||||
| 				if (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. | ||||
| 				let isFacebookBirthday = false; | ||||
| 				if (typeof event.uid !== "undefined") { | ||||
| 					if (event.uid.indexOf("@facebook.com") !== -1) { | ||||
| 						isFacebookBirthday = true; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if (event.type === "VEVENT") { | ||||
| 					let startDate = eventDate(event, "start"); | ||||
| 					let endDate; | ||||
|  | ||||
| 					Log.debug("\nevent=" + JSON.stringify(event)); | ||||
| 					if (typeof event.end !== "undefined") { | ||||
| 						endDate = eventDate(event, "end"); | ||||
| 					} else if (typeof event.duration !== "undefined") { | ||||
| 						endDate = startDate.clone().add(moment.duration(event.duration)); | ||||
| 					} else { | ||||
| 						if (!isFacebookBirthday) { | ||||
| 							// make copy of start date, separate storage area | ||||
| 							endDate = moment(startDate.format("x"), "x"); | ||||
| 						} else { | ||||
| 							endDate = moment(startDate).add(1, "days"); | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate()); | ||||
|  | ||||
| 					// calculate the duration of the event for use with recurring events. | ||||
| 					let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); | ||||
|  | ||||
| 					if (event.start.length === 8) { | ||||
| 						startDate = startDate.startOf("day"); | ||||
| 					} | ||||
|  | ||||
| 					const title = getTitleFromEvent(event); | ||||
|  | ||||
| 					let excluded = false, | ||||
| 						dateFilter = null; | ||||
|  | ||||
| 					for (let f in excludedEvents) { | ||||
| 						let 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) { | ||||
| 						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 (isFullDayEvent(event)) { | ||||
| 							// if full day event, only use the date part of the ranges | ||||
| 							pastLocal = pastMoment.toDate(); | ||||
| 							futureLocal = futureMoment.toDate(); | ||||
| 						} else { | ||||
| 							// if we want past events | ||||
| 							if (includePastEvents) { | ||||
| 								// use the calculated past time for the between from | ||||
| 								pastLocal = pastMoment.toDate(); | ||||
| 							} else { | ||||
| 								// otherwise use NOW.. cause we shouldnt use any before now | ||||
| 								pastLocal = moment().toDate(); //now | ||||
| 							} | ||||
| 							futureLocal = futureMoment.toDate(); // future | ||||
| 						} | ||||
| 						Log.debug(" between=" + pastLocal + " to " + futureLocal); | ||||
| 						const dates = rule.between(pastLocal, futureLocal, true, limitFunction); | ||||
| 						Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates)); | ||||
| 						// The "dates" array contains the set of dates within our desired date range range that are valid | ||||
| 						// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that | ||||
| 						// had its date changed from outside the range to inside the range.  For the time being, | ||||
| 						// we'll handle this by adding *all* recurrence entries into the set of dates that we check, | ||||
| 						// because the logic below will filter out any recurrences that don't actually belong within | ||||
| 						// our display range. | ||||
| 						// Would be great if there was a better way to handle this. | ||||
| 						if (event.recurrences !== undefined) { | ||||
| 							for (let r in event.recurrences) { | ||||
| 								// Only add dates that weren't already in the range we added from the rrule so that | ||||
| 								// we don"t double-add those events. | ||||
| 								if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) { | ||||
| 									dates.push(new Date(r)); | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 						// Loop through the set of date entries to see which recurrences should be added to our event list. | ||||
| 						for (let d in dates) { | ||||
| 							let date = dates[d]; | ||||
| 							// ical.js started returning recurrences and exdates as ISOStrings without time information. | ||||
| 							// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same | ||||
| 							// (see https://github.com/peterbraden/ical.js/pull/84 ) | ||||
| 							const dateKey = date.toISOString().substring(0, 10); | ||||
| 							let curEvent = event; | ||||
| 							let showRecurrence = true; | ||||
|  | ||||
| 							// for full day events, the time might be off from RRULE/Luxon problem | ||||
| 							if (isFullDayEvent(event)) { | ||||
| 								Log.debug("fullday"); | ||||
| 								// if the offset is  negative, east of GMT where the problem is | ||||
| 								if (date.getTimezoneOffset() < 0) { | ||||
| 									// get the offset of today where we are processing | ||||
| 									// this will be the correction we need to apply | ||||
| 									let nowOffset = new Date().getTimezoneOffset(); | ||||
| 									Log.debug("now offset is " + nowOffset); | ||||
| 									// reduce the time by the offset | ||||
| 									Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset()); | ||||
| 									// 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 date is " + date); | ||||
| 								} | ||||
| 							} | ||||
| 							startDate = moment(date); | ||||
|  | ||||
| 							let adjustDays = getCorrection(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 = 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) { | ||||
| 								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: 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 : isFullDayEvent(event); | ||||
| 						// Log.debug("full day event") | ||||
|  | ||||
| 						if (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 (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 = getCorrection(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 | ||||
| 						}); | ||||
| 					} | ||||
| 				try { | ||||
| 					data = ical.parseICS(responseData); | ||||
| 					Log.debug(`parsed data=${JSON.stringify(data)}`); | ||||
| 					events = CalendarFetcherUtils.filterEvents(data, { | ||||
| 						excludedEvents, | ||||
| 						includePastEvents, | ||||
| 						maximumEntries, | ||||
| 						maximumNumberOfDays | ||||
| 					}); | ||||
| 				} catch (error) { | ||||
| 					fetchFailedCallback(this, error); | ||||
| 					scheduleTimer(); | ||||
| 					return; | ||||
| 				} | ||||
| 				this.broadcastEvents(); | ||||
| 				scheduleTimer(); | ||||
| 			}) | ||||
| 			.catch((error) => { | ||||
| 				fetchFailedCallback(this, error); | ||||
| 				scheduleTimer(); | ||||
| 			}); | ||||
|  | ||||
| 			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(); | ||||
| 			var entries = 0; | ||||
| 			events = []; | ||||
| 			for (let ne of newEvents) { | ||||
| 				if (moment(ne.endDate, "x").isBefore(now)) { | ||||
| 					if (includePastEvents) events.push(ne); | ||||
| 					continue; | ||||
| 				} | ||||
| 				entries++; | ||||
| 				// If max events has been saved, skip the rest | ||||
| 				if (entries > maximumEntries) break; | ||||
| 				events.push(ne); | ||||
| 			} | ||||
|  | ||||
| 			self.broadcastEvents(); | ||||
| 			scheduleTimer(); | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
| 	/* | ||||
| 	 * | ||||
| 	 *	get the time correction, either dst/std or full day in cases where utc time is day before plus offset | ||||
| 	 * | ||||
| 	 */ | ||||
| 	const getCorrection = 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 = getIanaTZFromMS(event.start.tz); | ||||
| 				Log.debug("corrected TZ=" + tz); | ||||
| 				// watch out for unregistered windows timezone names | ||||
| 				// if we had a successfule 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 | ||||
| 				Log.debug("start date/time=" + moment(event.start).toDate()); | ||||
| 				start_offset = moment.tz(moment(event.start), 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; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * | ||||
| 	 *  lookup iana tz from windows | ||||
| 	 */ | ||||
| 	let zoneTable = null; | ||||
| 	const getIanaTZFromMS = function (msTZName) { | ||||
| 		if (!zoneTable) { | ||||
| 			const p = require("path"); | ||||
| 			zoneTable = require(p.join(__dirname, "windowsZones.json")); | ||||
| 		} | ||||
| 		// Get hash entry | ||||
| 		const he = zoneTable[msTZName]; | ||||
| 		// If found return iana name, else null | ||||
| 		return he ? he.iana[0] : null; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| @@ -537,82 +94,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
| 		}, reloadInterval); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * 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 | ||||
| 	 */ | ||||
| 	const 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 | ||||
| 	 */ | ||||
| 	const 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; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * 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. | ||||
| 	 */ | ||||
| 	const 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; | ||||
| 	}; | ||||
|  | ||||
| 	const 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 */ | ||||
|  | ||||
| 	/** | ||||
| @@ -626,13 +107,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
| 	 * Broadcast the existing events. | ||||
| 	 */ | ||||
| 	this.broadcastEvents = function () { | ||||
| 		Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events."); | ||||
| 		eventsReceivedCallback(self); | ||||
| 		Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`); | ||||
| 		eventsReceivedCallback(this); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Sets the on success callback | ||||
| 	 * | ||||
| 	 * @param {Function} callback The on success callback. | ||||
| 	 */ | ||||
| 	this.onReceive = function (callback) { | ||||
| @@ -641,7 +121,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
|  | ||||
| 	/** | ||||
| 	 * Sets the on error callback | ||||
| 	 * | ||||
| 	 * @param {Function} callback The on error callback. | ||||
| 	 */ | ||||
| 	this.onError = function (callback) { | ||||
| @@ -650,7 +129,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns the url of this fetcher. | ||||
| 	 * | ||||
| 	 * @returns {string} The url of this fetcher. | ||||
| 	 */ | ||||
| 	this.url = function () { | ||||
| @@ -659,7 +137,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns current available events for this fetcher. | ||||
| 	 * | ||||
| 	 * @returns {object[]} The current available events for this fetcher. | ||||
| 	 */ | ||||
| 	this.events = function () { | ||||
|   | ||||
							
								
								
									
										608
									
								
								modules/default/calendar/calendarfetcherutils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										608
									
								
								modules/default/calendar/calendarfetcherutils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,608 @@ | ||||
| /* MagicMirror² | ||||
|  * Calendar Fetcher Util Methods | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @external Moment | ||||
|  */ | ||||
| const path = require("path"); | ||||
| const moment = require("moment"); | ||||
| const zoneTable = require(path.join(__dirname, "windowsZones.json")); | ||||
| const Log = require("../../../js/logger"); | ||||
|  | ||||
| const CalendarFetcherUtils = { | ||||
| 	/** | ||||
| 	 * Calculate the time correction, either dst/std or full day in cases where | ||||
| 	 * utc time is day before plus offset | ||||
| 	 * @param {object} event the event which needs adjustment | ||||
| 	 * @param {Date} date the date on which this event happens | ||||
| 	 * @returns {number} the necessary adjustment in hours | ||||
| 	 */ | ||||
| 	calculateTimezoneAdjustment: function (event, date) { | ||||
| 		let adjustHours = 0; | ||||
| 		// if a timezone was specified | ||||
| 		if (!event.start.tz) { | ||||
| 			Log.debug(" if no tz, guess based on now"); | ||||
| 			event.start.tz = moment.tz.guess(); | ||||
| 		} | ||||
| 		Log.debug(`initial tz=${event.start.tz}`); | ||||
|  | ||||
| 		// if there is a start date specified | ||||
| 		if (event.start.tz) { | ||||
| 			// if this is a windows timezone | ||||
| 			if (event.start.tz.includes(" ")) { | ||||
| 				// use the lookup table to get theIANA name as moment and date don't know MS timezones | ||||
| 				let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz); | ||||
| 				Log.debug(`corrected TZ=${tz}`); | ||||
| 				// watch out for unregistered windows timezone names | ||||
| 				// if we had a successful lookup | ||||
| 				if (tz) { | ||||
| 					// change the timezone to the IANA name | ||||
| 					event.start.tz = tz; | ||||
| 					// Log.debug("corrected timezone="+event.start.tz) | ||||
| 				} | ||||
| 			} | ||||
| 			Log.debug(`corrected tz=${event.start.tz}`); | ||||
| 			let current_offset = 0; // offset  from TZ string or calculated | ||||
| 			let mm = 0; // date with tz or offset | ||||
| 			let start_offset = 0; // utc offset of created with tz | ||||
| 			// if there is still an offset, lookup failed, use it | ||||
| 			if (event.start.tz.startsWith("(")) { | ||||
| 				const regex = /[+|-]\d*:\d*/; | ||||
| 				const start_offsetString = event.start.tz.match(regex).toString().split(":"); | ||||
| 				let start_offset = parseInt(start_offsetString[0]); | ||||
| 				start_offset *= event.start.tz[1] === "-" ? -1 : 1; | ||||
| 				adjustHours = start_offset; | ||||
| 				Log.debug(`defined offset=${start_offset} hours`); | ||||
| 				current_offset = start_offset; | ||||
| 				event.start.tz = ""; | ||||
| 				Log.debug(`ical offset=${current_offset} date=${date}`); | ||||
| 				mm = moment(date); | ||||
| 				let x = parseInt(moment(new Date()).utcOffset()); | ||||
| 				Log.debug(`net mins=${current_offset * 60 - x}`); | ||||
|  | ||||
| 				mm = mm.add(x - current_offset * 60, "minutes"); | ||||
| 				adjustHours = (current_offset * 60 - x) / 60; | ||||
| 				event.start = mm.toDate(); | ||||
| 				Log.debug(`adjusted date=${event.start}`); | ||||
| 			} else { | ||||
| 				// get the start time in that timezone | ||||
| 				let es = moment(event.start); | ||||
| 				// check for start date prior to start of daylight changing date | ||||
| 				if (es.format("YYYY") < 2007) { | ||||
| 					es.set("year", 2013); // if so, use a closer date | ||||
| 				} | ||||
| 				Log.debug(`start date/time=${es.toDate()}`); | ||||
| 				start_offset = moment.tz(es, event.start.tz).utcOffset(); | ||||
| 				Log.debug(`start offset=${start_offset}`); | ||||
|  | ||||
| 				Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`); | ||||
|  | ||||
| 				// get the specified date in that timezone | ||||
| 				mm = moment.tz(moment(date), event.start.tz); | ||||
| 				Log.debug(`event date=${mm.toDate()}`); | ||||
| 				current_offset = mm.utcOffset(); | ||||
| 			} | ||||
| 			Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`); | ||||
|  | ||||
| 			// if the offset is greater than 0, east of london | ||||
| 			if (current_offset !== start_offset) { | ||||
| 				// big offset | ||||
| 				Log.debug("offset"); | ||||
| 				let h = parseInt(mm.format("H")); | ||||
| 				// check if the event time is less than the offset | ||||
| 				if (h > 0 && h < Math.abs(current_offset) / 60) { | ||||
| 					// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time) | ||||
| 					// we need to fix that | ||||
| 					//adjustHours = 24; | ||||
| 					// Log.debug("adjusting date") | ||||
| 				} | ||||
| 				//-300 > -240 | ||||
| 				//if (Math.abs(current_offset) > Math.abs(start_offset)){ | ||||
| 				if (current_offset > start_offset) { | ||||
| 					adjustHours -= 1; | ||||
| 					Log.debug("adjust down 1 hour dst change"); | ||||
| 					//} else if (Math.abs(current_offset) < Math.abs(start_offset)) { | ||||
| 				} else if (current_offset < start_offset) { | ||||
| 					adjustHours += 1; | ||||
| 					Log.debug("adjust up 1 hour dst change"); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		Log.debug(`adjustHours=${adjustHours}`); | ||||
| 		return adjustHours; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Filter the events from ical according to the given config | ||||
| 	 * @param {object} data the calendar data from ical | ||||
| 	 * @param {object} config The configuration object | ||||
| 	 * @returns {string[]} the filtered events | ||||
| 	 */ | ||||
| 	filterEvents: function (data, config) { | ||||
| 		const newEvents = []; | ||||
|  | ||||
| 		// limitFunction doesn't do much limiting, see comment re: the dates | ||||
| 		// array in rrule section below as to why we need to do the filtering | ||||
| 		// ourselves | ||||
| 		const limitFunction = function (date, i) { | ||||
| 			return true; | ||||
| 		}; | ||||
|  | ||||
| 		const eventDate = function (event, time) { | ||||
| 			return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time])); | ||||
| 		}; | ||||
|  | ||||
| 		Log.debug(`There are ${Object.entries(data).length} calendar entries.`); | ||||
| 		Object.entries(data).forEach(([key, event]) => { | ||||
| 			Log.debug("Processing entry..."); | ||||
| 			const now = new Date(); | ||||
| 			const today = moment().startOf("day").toDate(); | ||||
| 			const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat. | ||||
| 			let past = today; | ||||
|  | ||||
| 			if (config.includePastEvents) { | ||||
| 				past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate(); | ||||
| 			} | ||||
|  | ||||
| 			// FIXME: Ugly fix to solve the facebook birthday issue. | ||||
| 			// Otherwise, the recurring events only show the birthday for next year. | ||||
| 			let isFacebookBirthday = false; | ||||
| 			if (typeof event.uid !== "undefined") { | ||||
| 				if (event.uid.indexOf("@facebook.com") !== -1) { | ||||
| 					isFacebookBirthday = true; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (event.type === "VEVENT") { | ||||
| 				Log.debug(`Event:\n${JSON.stringify(event)}`); | ||||
| 				let startDate = eventDate(event, "start"); | ||||
| 				let endDate; | ||||
|  | ||||
| 				if (typeof event.end !== "undefined") { | ||||
| 					endDate = eventDate(event, "end"); | ||||
| 				} else if (typeof event.duration !== "undefined") { | ||||
| 					endDate = startDate.clone().add(moment.duration(event.duration)); | ||||
| 				} else { | ||||
| 					if (!isFacebookBirthday) { | ||||
| 						// make copy of start date, separate storage area | ||||
| 						endDate = moment(startDate.format("x"), "x"); | ||||
| 					} else { | ||||
| 						endDate = moment(startDate).add(1, "days"); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				Log.debug(`start: ${startDate.toDate()}`); | ||||
| 				Log.debug(`end:: ${endDate.toDate()}`); | ||||
|  | ||||
| 				// Calculate the duration of the event for use with recurring events. | ||||
| 				let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); | ||||
| 				Log.debug(`duration: ${duration}`); | ||||
|  | ||||
| 				// FIXME: Since the parsed json object from node-ical comes with time information | ||||
| 				// this check could be removed (?) | ||||
| 				if (event.start.length === 8) { | ||||
| 					startDate = startDate.startOf("day"); | ||||
| 				} | ||||
|  | ||||
| 				const title = CalendarFetcherUtils.getTitleFromEvent(event); | ||||
| 				Log.debug(`title: ${title}`); | ||||
|  | ||||
| 				let excluded = false, | ||||
| 					dateFilter = null; | ||||
|  | ||||
| 				for (let f in config.excludedEvents) { | ||||
| 					let filter = config.excludedEvents[f], | ||||
| 						testTitle = title.toLowerCase(), | ||||
| 						until = null, | ||||
| 						useRegex = false, | ||||
| 						regexFlags = "g"; | ||||
|  | ||||
| 					if (filter instanceof Object) { | ||||
| 						if (typeof filter.until !== "undefined") { | ||||
| 							until = filter.until; | ||||
| 						} | ||||
|  | ||||
| 						if (typeof filter.regex !== "undefined") { | ||||
| 							useRegex = filter.regex; | ||||
| 						} | ||||
|  | ||||
| 						// If additional advanced filtering is added in, this section | ||||
| 						// must remain last as we overwrite the filter object with the | ||||
| 						// filterBy string | ||||
| 						if (filter.caseSensitive) { | ||||
| 							filter = filter.filterBy; | ||||
| 							testTitle = title; | ||||
| 						} else if (useRegex) { | ||||
| 							filter = filter.filterBy; | ||||
| 							testTitle = title; | ||||
| 							regexFlags += "i"; | ||||
| 						} else { | ||||
| 							filter = filter.filterBy.toLowerCase(); | ||||
| 						} | ||||
| 					} else { | ||||
| 						filter = filter.toLowerCase(); | ||||
| 					} | ||||
|  | ||||
| 					if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) { | ||||
| 						if (until) { | ||||
| 							dateFilter = until; | ||||
| 						} else { | ||||
| 							excluded = true; | ||||
| 						} | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if (excluded) { | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				const location = event.location || false; | ||||
| 				const geo = event.geo || false; | ||||
| 				const description = event.description || false; | ||||
|  | ||||
| 				if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) { | ||||
| 					const rule = event.rrule; | ||||
| 					let addedEvents = 0; | ||||
|  | ||||
| 					const pastMoment = moment(past); | ||||
| 					const futureMoment = moment(future); | ||||
|  | ||||
| 					// can cause problems with e.g. birthdays before 1900 | ||||
| 					if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) { | ||||
| 						rule.origOptions.dtstart.setYear(1900); | ||||
| 						rule.options.dtstart.setYear(1900); | ||||
| 					} | ||||
|  | ||||
| 					// For recurring events, get the set of start dates that fall within the range | ||||
| 					// of dates we're looking for. | ||||
| 					// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time | ||||
| 					let pastLocal = 0; | ||||
| 					let futureLocal = 0; | ||||
| 					if (CalendarFetcherUtils.isFullDayEvent(event)) { | ||||
| 						Log.debug("fullday"); | ||||
| 						// if full day event, only use the date part of the ranges | ||||
| 						pastLocal = pastMoment.toDate(); | ||||
| 						futureLocal = futureMoment.toDate(); | ||||
|  | ||||
| 						Log.debug(`pastLocal: ${pastLocal}`); | ||||
| 						Log.debug(`futureLocal: ${futureLocal}`); | ||||
| 					} else { | ||||
| 						// if we want past events | ||||
| 						if (config.includePastEvents) { | ||||
| 							// use the calculated past time for the between from | ||||
| 							pastLocal = pastMoment.toDate(); | ||||
| 						} else { | ||||
| 							// otherwise use NOW.. cause we shouldn't use any before now | ||||
| 							pastLocal = moment().toDate(); //now | ||||
| 						} | ||||
| 						futureLocal = futureMoment.toDate(); // future | ||||
| 					} | ||||
| 					Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`); | ||||
| 					const dates = rule.between(pastLocal, futureLocal, true, limitFunction); | ||||
| 					Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`); | ||||
| 					// The "dates" array contains the set of dates within our desired date range range that are valid | ||||
| 					// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that | ||||
| 					// had its date changed from outside the range to inside the range.  For the time being, | ||||
| 					// we'll handle this by adding *all* recurrence entries into the set of dates that we check, | ||||
| 					// because the logic below will filter out any recurrences that don't actually belong within | ||||
| 					// our display range. | ||||
| 					// Would be great if there was a better way to handle this. | ||||
| 					Log.debug(`event.recurrences: ${event.recurrences}`); | ||||
| 					if (event.recurrences !== undefined) { | ||||
| 						for (let r in event.recurrences) { | ||||
| 							// Only add dates that weren't already in the range we added from the rrule so that | ||||
| 							// we don"t double-add those events. | ||||
| 							if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) { | ||||
| 								dates.push(new Date(r)); | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 					// Loop through the set of date entries to see which recurrences should be added to our event list. | ||||
| 					for (let d in dates) { | ||||
| 						let date = dates[d]; | ||||
| 						// Remove the time information of each date by using its substring, using the following method: | ||||
| 						// .toISOString().substring(0,10). | ||||
| 						// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ | ||||
| 						// (see https://momentjs.com/docs/#/displaying/as-iso-string/). | ||||
| 						const dateKey = date.toISOString().substring(0, 10); | ||||
| 						let curEvent = event; | ||||
| 						let showRecurrence = true; | ||||
|  | ||||
| 						// set the time information in the date to equal the time information in the event | ||||
| 						date.setUTCHours(curEvent.start.getUTCHours(), curEvent.start.getUTCMinutes(), curEvent.start.getUTCSeconds(), curEvent.start.getUTCMilliseconds()); | ||||
|  | ||||
| 						// Get the offset of today where we are processing | ||||
| 						// This will be the correction, we need to apply. | ||||
| 						let nowOffset = new Date().getTimezoneOffset(); | ||||
| 						// For full day events, the time might be off from RRULE/Luxon problem | ||||
| 						// Get time zone offset of the rule calculated event | ||||
| 						let dateoffset = date.getTimezoneOffset(); | ||||
|  | ||||
| 						// Reduce the time by the following offset. | ||||
| 						Log.debug(` recurring date is ${date} offset is ${dateoffset}`); | ||||
|  | ||||
| 						let dh = moment(date).format("HH"); | ||||
| 						Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`); | ||||
|  | ||||
| 						if (CalendarFetcherUtils.isFullDayEvent(event)) { | ||||
| 							Log.debug("Fullday"); | ||||
| 							// If the offset is negative (east of GMT), where the problem is | ||||
| 							if (dateoffset < 0) { | ||||
| 								if (dh < Math.abs(dateoffset / 60)) { | ||||
| 									// if the rrule byweekday WAS explicitly set , correct it | ||||
| 									// reduce the time by the offset | ||||
| 									if (curEvent.rrule.origOptions.byweekday !== undefined) { | ||||
| 										// Apply the correction to the date/time to get it UTC relative | ||||
| 										date = new Date(date.getTime() - Math.abs(24 * 60) * 60000); | ||||
| 									} | ||||
| 									// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 									// fix it for this event entry | ||||
| 									//duration = 24 * 60 * 60 * 1000; | ||||
| 									Log.debug(`new recurring date1 fulldate is ${date}`); | ||||
| 								} | ||||
| 							} else { | ||||
| 								// if the timezones are the same, correct date if needed | ||||
| 								//if (event.start.tz === moment.tz.guess()) { | ||||
| 								// if the date hour is less than the offset | ||||
| 								if (24 - dh <= Math.abs(dateoffset / 60)) { | ||||
| 									// if the rrule byweekday WAS explicitly set , correct it | ||||
| 									if (curEvent.rrule.origOptions.byweekday !== undefined) { | ||||
| 										// apply the correction to the date/time back to right day | ||||
| 										date = new Date(date.getTime() + Math.abs(24 * 60) * 60000); | ||||
| 									} | ||||
| 									// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 									// fix it for this event entry | ||||
| 									//duration = 24 * 60 * 60 * 1000; | ||||
| 									Log.debug(`new recurring date2 fulldate is ${date}`); | ||||
| 								} | ||||
| 								//} | ||||
| 							} | ||||
| 						} else { | ||||
| 							// not full day, but luxon can still screw up the date on the rule processing | ||||
| 							// we need to correct the date to get back to the right event for | ||||
| 							if (dateoffset < 0) { | ||||
| 								// if the date hour is less than the offset | ||||
| 								if (dh <= Math.abs(dateoffset / 60)) { | ||||
| 									// if the rrule byweekday WAS explicitly set , correct it | ||||
| 									if (curEvent.rrule.origOptions.byweekday !== undefined) { | ||||
| 										// Reduce the time by t: | ||||
| 										// Apply the correction to the date/time to get it UTC relative | ||||
| 										date = new Date(date.getTime() - Math.abs(24 * 60) * 60000); | ||||
| 									} | ||||
| 									// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 									// fix it for this event entry | ||||
| 									//duration = 24 * 60 * 60 * 1000; | ||||
| 									Log.debug(`new recurring date1 is ${date}`); | ||||
| 								} | ||||
| 							} else { | ||||
| 								// if the timezones are the same, correct date if needed | ||||
| 								//if (event.start.tz === moment.tz.guess()) { | ||||
| 								// if the date hour is less than the offset | ||||
| 								if (24 - dh <= Math.abs(dateoffset / 60)) { | ||||
| 									// if the rrule byweekday WAS explicitly set , correct it | ||||
| 									if (curEvent.rrule.origOptions.byweekday !== undefined) { | ||||
| 										// apply the correction to the date/time back to right day | ||||
| 										date = new Date(date.getTime() + Math.abs(24 * 60) * 60000); | ||||
| 									} | ||||
| 									// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 									// fix it for this event entry | ||||
| 									//duration = 24 * 60 * 60 * 1000; | ||||
| 									Log.debug(`new recurring date2 is ${date}`); | ||||
| 								} | ||||
| 								//} | ||||
| 							} | ||||
| 						} | ||||
| 						startDate = moment(date); | ||||
| 						Log.debug(`Corrected startDate: ${startDate.toDate()}`); | ||||
|  | ||||
| 						let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date); | ||||
|  | ||||
| 						// For each date that we're checking, it's possible that there is a recurrence override for that one day. | ||||
| 						if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) { | ||||
| 							// We found an override, so for this recurrence, use a potentially different title, start date, and duration. | ||||
| 							curEvent = curEvent.recurrences[dateKey]; | ||||
| 							startDate = moment(curEvent.start); | ||||
| 							duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x")); | ||||
| 						} | ||||
| 						// If there's no recurrence override, check for an exception date.  Exception dates represent exceptions to the rule. | ||||
| 						else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) { | ||||
| 							// This date is an exception date, which means we should skip it in the recurrence pattern. | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
| 						Log.debug(`duration: ${duration}`); | ||||
|  | ||||
| 						endDate = moment(parseInt(startDate.format("x")) + duration, "x"); | ||||
| 						if (startDate.format("x") === endDate.format("x")) { | ||||
| 							endDate = endDate.endOf("day"); | ||||
| 						} | ||||
|  | ||||
| 						const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent); | ||||
|  | ||||
| 						// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add | ||||
| 						// it to the event list. | ||||
| 						if (endDate.isBefore(past) || startDate.isAfter(future)) { | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
|  | ||||
| 						if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) { | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
|  | ||||
| 						if (showRecurrence === true) { | ||||
| 							Log.debug(`saving event: ${description}`); | ||||
| 							addedEvents++; | ||||
| 							newEvents.push({ | ||||
| 								title: recurrenceTitle, | ||||
| 								startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), | ||||
| 								endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), | ||||
| 								fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), | ||||
| 								recurringEvent: true, | ||||
| 								class: event.class, | ||||
| 								firstYear: event.start.getFullYear(), | ||||
| 								location: location, | ||||
| 								geo: geo, | ||||
| 								description: description | ||||
| 							}); | ||||
| 						} | ||||
| 					} | ||||
| 					// End recurring event parsing. | ||||
| 				} else { | ||||
| 					// Single event. | ||||
| 					const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event); | ||||
| 					// Log.debug("full day event") | ||||
|  | ||||
| 					// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00) | ||||
| 					if (fullDayEvent && startDate.format("x") === endDate.format("x")) { | ||||
| 						endDate = endDate.endOf("day"); | ||||
| 					} | ||||
|  | ||||
| 					if (config.includePastEvents) { | ||||
| 						// Past event is too far in the past, so skip. | ||||
| 						if (endDate < past) { | ||||
| 							return; | ||||
| 						} | ||||
| 					} else { | ||||
| 						// It's not a fullday event, and it is in the past, so skip. | ||||
| 						if (!fullDayEvent && endDate < new Date()) { | ||||
| 							return; | ||||
| 						} | ||||
|  | ||||
| 						// It's a fullday event, and it is before today, So skip. | ||||
| 						if (fullDayEvent && endDate <= today) { | ||||
| 							return; | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					// It exceeds the maximumNumberOfDays limit, so skip. | ||||
| 					if (startDate > future) { | ||||
| 						return; | ||||
| 					} | ||||
|  | ||||
| 					if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) { | ||||
| 						return; | ||||
| 					} | ||||
|  | ||||
| 					// get correction for date saving and dst change between now and then | ||||
| 					let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate()); | ||||
| 					// Every thing is good. Add it to the list. | ||||
| 					newEvents.push({ | ||||
| 						title: title, | ||||
| 						startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), | ||||
| 						endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), | ||||
| 						fullDayEvent: fullDayEvent, | ||||
| 						class: event.class, | ||||
| 						location: location, | ||||
| 						geo: geo, | ||||
| 						description: description | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		newEvents.sort(function (a, b) { | ||||
| 			return a.startDate - b.startDate; | ||||
| 		}); | ||||
|  | ||||
| 		return newEvents; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Lookup iana tz from windows | ||||
| 	 * @param {string} msTZName the timezone name to lookup | ||||
| 	 * @returns {string|null} the iana name or null of none is found | ||||
| 	 */ | ||||
| 	getIanaTZFromMS: function (msTZName) { | ||||
| 		// Get hash entry | ||||
| 		const he = zoneTable[msTZName]; | ||||
| 		// If found return iana name, else null | ||||
| 		return he ? he.iana[0] : null; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Gets the title from the event. | ||||
| 	 * @param {object} event The event object to check. | ||||
| 	 * @returns {string} The title of the event, or "Event" if no title is found. | ||||
| 	 */ | ||||
| 	getTitleFromEvent: function (event) { | ||||
| 		let title = "Event"; | ||||
| 		if (event.summary) { | ||||
| 			title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary; | ||||
| 		} else if (event.description) { | ||||
| 			title = event.description; | ||||
| 		} | ||||
|  | ||||
| 		return title; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks if an event is a fullday event. | ||||
| 	 * @param {object} event The event object to check. | ||||
| 	 * @returns {boolean} True if the event is a fullday event, false otherwise | ||||
| 	 */ | ||||
| 	isFullDayEvent: function (event) { | ||||
| 		if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		const start = event.start || 0; | ||||
| 		const startDate = new Date(start); | ||||
| 		const end = event.end || 0; | ||||
| 		if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) { | ||||
| 			// Is 24 hours, and starts on the middle of the night. | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Determines if the user defined time filter should apply | ||||
| 	 * @param {Date} now Date object using previously created object for consistency | ||||
| 	 * @param {Moment} endDate Moment object representing the event end date | ||||
| 	 * @param {string} filter The time to subtract from the end date to determine if an event should be shown | ||||
| 	 * @returns {boolean} True if the event should be filtered out, false otherwise | ||||
| 	 */ | ||||
| 	timeFilterApplies: function (now, endDate, filter) { | ||||
| 		if (filter) { | ||||
| 			const until = filter.split(" "), | ||||
| 				value = parseInt(until[0]), | ||||
| 				increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js | ||||
| 				filterUntil = moment(endDate.format()).subtract(value, increment); | ||||
|  | ||||
| 			return now < filterUntil.format("x"); | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Determines if the user defined title filter should apply | ||||
| 	 * @param {string} title the title of the event | ||||
| 	 * @param {string} filter the string to look for, can be a regex also | ||||
| 	 * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string | ||||
| 	 * @param {string} regexFlags flags that should be applied to the regex | ||||
| 	 * @returns {boolean} True if the title should be filtered out, false otherwise | ||||
| 	 */ | ||||
| 	titleFilterApplies: function (title, filter, useRegex, regexFlags) { | ||||
| 		if (useRegex) { | ||||
| 			let regexFilter = filter; | ||||
| 			// Assume if leading slash, there is also trailing slash | ||||
| 			if (filter[0] === "/") { | ||||
| 				// Strip leading and trailing slashes | ||||
| 				regexFilter = filter.substr(1).slice(0, -1); | ||||
| 			} | ||||
| 			return new RegExp(regexFilter, regexFlags).test(title); | ||||
| 		} else { | ||||
| 			return title.includes(filter); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| if (typeof module !== "undefined") { | ||||
| 	module.exports = CalendarFetcherUtils; | ||||
| } | ||||
							
								
								
									
										117
									
								
								modules/default/calendar/calendarutils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								modules/default/calendar/calendarutils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| /* MagicMirror² | ||||
|  * Calendar Util Methods | ||||
|  * | ||||
|  * By Rejas | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const CalendarUtils = { | ||||
| 	/** | ||||
| 	 * Capitalize the first letter of a string | ||||
| 	 * @param {string} string The string to capitalize | ||||
| 	 * @returns {string} The capitalized string | ||||
| 	 */ | ||||
| 	capFirst: function (string) { | ||||
| 		return string.charAt(0).toUpperCase() + string.slice(1); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the | ||||
| 	 * corresponding time-format to be used in the calendar display. If no number is given (or otherwise invalid input) | ||||
| 	 * it will a localeSpecification object with the system locale time format. | ||||
| 	 * @param {number} timeFormat Specifies either 12 or 24-hour time format | ||||
| 	 * @returns {moment.LocaleSpecification} formatted time | ||||
| 	 */ | ||||
| 	getLocaleSpecification: function (timeFormat) { | ||||
| 		switch (timeFormat) { | ||||
| 			case 12: { | ||||
| 				return { longDateFormat: { LT: "h:mm A" } }; | ||||
| 			} | ||||
| 			case 24: { | ||||
| 				return { longDateFormat: { LT: "HH:mm" } }; | ||||
| 			} | ||||
| 			default: { | ||||
| 				return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } }; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Shortens a string if it's longer than maxLength and add an ellipsis to the end | ||||
| 	 * @param {string} string Text string to shorten | ||||
| 	 * @param {number} maxLength The max length of the string | ||||
| 	 * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength | ||||
| 	 * @param {number} maxTitleLines The max number of vertical lines before cutting event title | ||||
| 	 * @returns {string} The shortened string | ||||
| 	 */ | ||||
| 	shorten: function (string, maxLength, wrapEvents, maxTitleLines) { | ||||
| 		if (typeof string !== "string") { | ||||
| 			return ""; | ||||
| 		} | ||||
|  | ||||
| 		if (wrapEvents === true) { | ||||
| 			const words = string.split(" "); | ||||
| 			let temp = ""; | ||||
| 			let currentLine = ""; | ||||
| 			let line = 0; | ||||
|  | ||||
| 			for (let i = 0; i < words.length; i++) { | ||||
| 				const word = words[i]; | ||||
| 				if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { | ||||
| 					// max - 1 to account for a space | ||||
| 					currentLine += `${word} `; | ||||
| 				} else { | ||||
| 					line++; | ||||
| 					if (line > maxTitleLines - 1) { | ||||
| 						if (i < words.length) { | ||||
| 							currentLine += "…"; | ||||
| 						} | ||||
| 						break; | ||||
| 					} | ||||
|  | ||||
| 					if (currentLine.length > 0) { | ||||
| 						temp += `${currentLine}<br>${word} `; | ||||
| 					} else { | ||||
| 						temp += `${word}<br>`; | ||||
| 					} | ||||
| 					currentLine = ""; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			return (temp + currentLine).trim(); | ||||
| 		} else { | ||||
| 			if (maxLength && typeof maxLength === "number" && string.length > maxLength) { | ||||
| 				return `${string.trim().slice(0, maxLength)}…`; | ||||
| 			} else { | ||||
| 				return string.trim(); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Transforms the title of an event for usage. | ||||
| 	 * Replaces parts of the text as defined in config.titleReplace. | ||||
| 	 * Shortens title based on config.maxTitleLength and config.wrapEvents | ||||
| 	 * @param {string} title The title to transform. | ||||
| 	 * @param {object} titleReplace Pairs of strings to be replaced in the title | ||||
| 	 * @returns {string} The transformed title. | ||||
| 	 */ | ||||
| 	titleTransform: function (title, titleReplace) { | ||||
| 		let transformedTitle = title; | ||||
| 		for (let needle in titleReplace) { | ||||
| 			const replacement = titleReplace[needle]; | ||||
|  | ||||
| 			const regParts = needle.match(/^\/(.+)\/([gim]*)$/); | ||||
| 			if (regParts) { | ||||
| 				// the parsed pattern is a regexp. | ||||
| 				needle = new RegExp(regParts[1], regParts[2]); | ||||
| 			} | ||||
|  | ||||
| 			transformedTitle = transformedTitle.replace(needle, replacement); | ||||
| 		} | ||||
| 		return transformedTitle; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| if (typeof module !== "undefined") { | ||||
| 	module.exports = CalendarUtils; | ||||
| } | ||||
| @@ -1,11 +1,14 @@ | ||||
| /* CalendarFetcher Tester | ||||
|  * use this script with `node debug.js` to test the fetcher without the need | ||||
|  * of starting the MagicMirror core. Adjust the values below to your desire. | ||||
|  * of starting the MagicMirror² core. Adjust the values below to your desire. | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const CalendarFetcher = require("./calendarfetcher.js"); | ||||
| // Alias modules mentioned in package.js under _moduleAliases. | ||||
| require("module-alias/register"); | ||||
|  | ||||
| const CalendarFetcher = require("./calendarfetcher"); | ||||
|  | ||||
| const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL | ||||
| //const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured  in Google OAuth2 first) | ||||
| @@ -26,11 +29,13 @@ const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maxi | ||||
| fetcher.onReceive(function (fetcher) { | ||||
| 	console.log(fetcher.events()); | ||||
| 	console.log("------------------------------------------------------------"); | ||||
| 	process.exit(0); | ||||
| }); | ||||
|  | ||||
| fetcher.onError(function (fetcher, error) { | ||||
| 	console.log("Fetcher error:"); | ||||
| 	console.log(error); | ||||
| 	process.exit(1); | ||||
| }); | ||||
|  | ||||
| fetcher.startFetch(); | ||||
|   | ||||
| @@ -1,32 +1,38 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Node Helper: Calendar | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const NodeHelper = require("node_helper"); | ||||
| const validUrl = require("valid-url"); | ||||
| const CalendarFetcher = require("./calendarfetcher.js"); | ||||
| const Log = require("../../../js/logger"); | ||||
| const Log = require("logger"); | ||||
| const CalendarFetcher = require("./calendarfetcher"); | ||||
|  | ||||
| module.exports = NodeHelper.create({ | ||||
| 	// Override start method. | ||||
| 	start: function () { | ||||
| 		Log.log("Starting node helper for: " + this.name); | ||||
| 		Log.log(`Starting node helper for: ${this.name}`); | ||||
| 		this.fetchers = []; | ||||
| 	}, | ||||
|  | ||||
| 	// Override socketNotificationReceived method. | ||||
| 	socketNotificationReceived: function (notification, payload) { | ||||
| 		if (notification === "ADD_CALENDAR") { | ||||
| 			this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id); | ||||
| 			this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id); | ||||
| 		} else if (notification === "FETCH_CALENDAR") { | ||||
| 			const key = payload.id + payload.url; | ||||
| 			if (typeof this.fetchers[key] === "undefined") { | ||||
| 				Log.error("Calendar Error. No fetcher exists with key: ", key); | ||||
| 				this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_UNSPECIFIED" }); | ||||
| 				return; | ||||
| 			} | ||||
| 			this.fetchers[key].startFetch(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Creates a fetcher for a new url if it doesn't exist yet. | ||||
| 	 * Otherwise it reuses the existing one. | ||||
| 	 * | ||||
| 	 * @param {string} url The url of the calendar | ||||
| 	 * @param {number} fetchInterval How often does the calendar needs to be fetched in ms | ||||
| 	 * @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown. | ||||
| @@ -34,44 +40,56 @@ module.exports = NodeHelper.create({ | ||||
| 	 * @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, identifier) { | ||||
| 		var self = this; | ||||
|  | ||||
| 		if (!validUrl.isUri(url)) { | ||||
| 			self.sendSocketNotification("INCORRECT_URL", { id: identifier, 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[identifier + url] === "undefined") { | ||||
| 			Log.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) { | ||||
| 				self.sendSocketNotification("CALENDAR_EVENTS", { | ||||
| 					id: identifier, | ||||
| 					url: fetcher.url(), | ||||
| 					events: fetcher.events() | ||||
| 				}); | ||||
| 			fetcher.onReceive((fetcher) => { | ||||
| 				this.broadcastEvents(fetcher, identifier); | ||||
| 			}); | ||||
|  | ||||
| 			fetcher.onError(function (fetcher, error) { | ||||
| 			fetcher.onError((fetcher, error) => { | ||||
| 				Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error); | ||||
| 				self.sendSocketNotification("FETCH_ERROR", { | ||||
| 				let error_type = NodeHelper.checkFetchError(error); | ||||
| 				this.sendSocketNotification("CALENDAR_ERROR", { | ||||
| 					id: identifier, | ||||
| 					url: fetcher.url(), | ||||
| 					error: error | ||||
| 					error_type | ||||
| 				}); | ||||
| 			}); | ||||
|  | ||||
| 			self.fetchers[identifier + url] = fetcher; | ||||
| 			this.fetchers[identifier + url] = fetcher; | ||||
| 		} else { | ||||
| 			Log.log("Use existing calendar fetcher for url: " + url); | ||||
| 			fetcher = self.fetchers[identifier + 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,6 +1,6 @@ | ||||
| # Module: Clock | ||||
|  | ||||
| The `clock` module is one of the default modules of the MagicMirror. | ||||
| The `clock` module is one of the default modules of the MagicMirror². | ||||
| This module displays the current date and time. The information will be updated realtime. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/clock.html). | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| /* global SunCalc */ | ||||
| /* global SunCalc, formatTime */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Module: Clock | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
| @@ -12,21 +12,24 @@ Module.register("clock", { | ||||
| 		displayType: "digital", // options: digital, analog, both | ||||
|  | ||||
| 		timeFormat: config.timeFormat, | ||||
| 		timezone: null, | ||||
|  | ||||
| 		displaySeconds: true, | ||||
| 		showPeriod: true, | ||||
| 		showPeriodUpper: false, | ||||
| 		clockBold: false, | ||||
| 		showDate: true, | ||||
| 		showTime: true, | ||||
| 		showWeek: false, | ||||
| 		dateFormat: "dddd, LL", | ||||
| 		sendNotifications: false, | ||||
|  | ||||
| 		/* specific to the analog clock */ | ||||
| 		analogSize: "200px", | ||||
| 		analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive) | ||||
| 		analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right' | ||||
| 		analogShowDate: "top", // options: false, 'top', or 'bottom' | ||||
| 		analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom' | ||||
| 		secondsColor: "#888888", | ||||
| 		timezone: null, | ||||
|  | ||||
| 		showSunTimes: false, | ||||
| 		showMoonTimes: false, | ||||
| @@ -43,69 +46,82 @@ Module.register("clock", { | ||||
| 	}, | ||||
| 	// Define start sequence. | ||||
| 	start: function () { | ||||
| 		Log.info("Starting module: " + this.name); | ||||
| 		Log.info(`Starting module: ${this.name}`); | ||||
|  | ||||
| 		// Schedule update interval. | ||||
| 		var self = this; | ||||
| 		self.second = moment().second(); | ||||
| 		self.minute = moment().minute(); | ||||
| 		this.second = moment().second(); | ||||
| 		this.minute = moment().minute(); | ||||
|  | ||||
| 		//Calculate how many ms should pass until next update depending on if seconds is displayed or not | ||||
| 		var delayCalculator = function (reducedSeconds) { | ||||
| 			var EXTRA_DELAY = 50; //Deliberate imperceptable delay to prevent off-by-one timekeeping errors | ||||
| 		// Calculate how many ms should pass until next update depending on if seconds is displayed or not | ||||
| 		const delayCalculator = (reducedSeconds) => { | ||||
| 			const EXTRA_DELAY = 50; // Deliberate imperceptible delay to prevent off-by-one timekeeping errors | ||||
|  | ||||
| 			if (self.config.displaySeconds) { | ||||
| 			if (this.config.displaySeconds) { | ||||
| 				return 1000 - moment().milliseconds() + EXTRA_DELAY; | ||||
| 			} else { | ||||
| 				return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY; | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		//A recursive timeout function instead of interval to avoid drifting | ||||
| 		var notificationTimer = function () { | ||||
| 			self.updateDom(); | ||||
| 		// A recursive timeout function instead of interval to avoid drifting | ||||
| 		const notificationTimer = () => { | ||||
| 			this.updateDom(); | ||||
|  | ||||
| 			//If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent) | ||||
| 			if (self.config.displaySeconds) { | ||||
| 				self.second = moment().second(); | ||||
| 				if (self.second !== 0) { | ||||
| 					self.sendNotification("CLOCK_SECOND", self.second); | ||||
| 					setTimeout(notificationTimer, delayCalculator(0)); | ||||
| 					return; | ||||
| 			if (this.config.sendNotifications) { | ||||
| 				// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent) | ||||
| 				if (this.config.displaySeconds) { | ||||
| 					this.second = moment().second(); | ||||
| 					if (this.second !== 0) { | ||||
| 						this.sendNotification("CLOCK_SECOND", this.second); | ||||
| 						setTimeout(notificationTimer, delayCalculator(0)); | ||||
| 						return; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification | ||||
| 				this.minute = moment().minute(); | ||||
| 				this.sendNotification("CLOCK_MINUTE", this.minute); | ||||
| 			} | ||||
|  | ||||
| 			//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification | ||||
| 			self.minute = moment().minute(); | ||||
| 			self.sendNotification("CLOCK_MINUTE", self.minute); | ||||
| 			setTimeout(notificationTimer, delayCalculator(0)); | ||||
| 		}; | ||||
|  | ||||
| 		//Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes | ||||
| 		setTimeout(notificationTimer, delayCalculator(self.second)); | ||||
| 		// Set the initial timeout with the amount of seconds elapsed as | ||||
| 		// reducedSeconds, so it will trigger when the minute changes | ||||
| 		setTimeout(notificationTimer, delayCalculator(this.second)); | ||||
|  | ||||
| 		// Set locale. | ||||
| 		moment.locale(config.language); | ||||
| 	}, | ||||
| 	// Override dom generator. | ||||
| 	getDom: function () { | ||||
| 		var wrapper = document.createElement("div"); | ||||
| 		const wrapper = document.createElement("div"); | ||||
| 		wrapper.classList.add("clock-grid"); | ||||
|  | ||||
| 		/************************************ | ||||
| 		 * Create wrappers for analog and digital clock | ||||
| 		 */ | ||||
| 		const analogWrapper = document.createElement("div"); | ||||
| 		analogWrapper.className = "clock-circle"; | ||||
| 		const digitalWrapper = document.createElement("div"); | ||||
| 		digitalWrapper.className = "digital"; | ||||
| 		digitalWrapper.style.gridArea = "center"; | ||||
|  | ||||
| 		/************************************ | ||||
| 		 * Create wrappers for DIGITAL clock | ||||
| 		 */ | ||||
| 		const dateWrapper = document.createElement("div"); | ||||
| 		const timeWrapper = document.createElement("div"); | ||||
| 		const secondsWrapper = document.createElement("sup"); | ||||
| 		const periodWrapper = document.createElement("span"); | ||||
| 		const sunWrapper = document.createElement("div"); | ||||
| 		const moonWrapper = document.createElement("div"); | ||||
| 		const weekWrapper = document.createElement("div"); | ||||
|  | ||||
| 		var dateWrapper = document.createElement("div"); | ||||
| 		var timeWrapper = document.createElement("div"); | ||||
| 		var secondsWrapper = document.createElement("sup"); | ||||
| 		var periodWrapper = document.createElement("span"); | ||||
| 		var sunWrapper = document.createElement("div"); | ||||
| 		var moonWrapper = document.createElement("div"); | ||||
| 		var weekWrapper = document.createElement("div"); | ||||
| 		// Style Wrappers | ||||
| 		dateWrapper.className = "date normal medium"; | ||||
| 		timeWrapper.className = "time bright large light"; | ||||
| 		secondsWrapper.className = "dimmed"; | ||||
| 		secondsWrapper.className = "seconds dimmed"; | ||||
| 		sunWrapper.className = "sun dimmed small"; | ||||
| 		moonWrapper.className = "moon dimmed small"; | ||||
| 		weekWrapper.className = "week dimmed medium"; | ||||
| @@ -114,63 +130,52 @@ Module.register("clock", { | ||||
| 		// The moment().format("h") method has a bug on the Raspberry Pi. | ||||
| 		// So we need to generate the timestring manually. | ||||
| 		// See issue: https://github.com/MichMich/MagicMirror/issues/181 | ||||
| 		var timeString; | ||||
| 		var now = moment(); | ||||
| 		this.lastDisplayedMinute = now.minute(); | ||||
| 		let timeString; | ||||
| 		const now = moment(); | ||||
| 		if (this.config.timezone) { | ||||
| 			now.tz(this.config.timezone); | ||||
| 		} | ||||
|  | ||||
| 		var hourSymbol = "HH"; | ||||
| 		let hourSymbol = "HH"; | ||||
| 		if (this.config.timeFormat !== 24) { | ||||
| 			hourSymbol = "h"; | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.clockBold === true) { | ||||
| 			timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]'); | ||||
| 		if (this.config.clockBold) { | ||||
| 			timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`); | ||||
| 		} else { | ||||
| 			timeString = now.format(hourSymbol + ":mm"); | ||||
| 			timeString = now.format(`${hourSymbol}:mm`); | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.showDate) { | ||||
| 			dateWrapper.innerHTML = now.format(this.config.dateFormat); | ||||
| 		} | ||||
| 		if (this.config.showWeek) { | ||||
| 			weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() }); | ||||
| 		} | ||||
| 		timeWrapper.innerHTML = timeString; | ||||
| 		secondsWrapper.innerHTML = now.format("ss"); | ||||
| 		if (this.config.showPeriodUpper) { | ||||
| 			periodWrapper.innerHTML = now.format("A"); | ||||
| 		} else { | ||||
| 			periodWrapper.innerHTML = now.format("a"); | ||||
| 		} | ||||
| 		if (this.config.displaySeconds) { | ||||
| 			timeWrapper.appendChild(secondsWrapper); | ||||
| 		} | ||||
| 		if (this.config.showPeriod && this.config.timeFormat !== 24) { | ||||
| 			timeWrapper.appendChild(periodWrapper); | ||||
| 			digitalWrapper.appendChild(dateWrapper); | ||||
| 		} | ||||
|  | ||||
| 		/** | ||||
| 		 * Format the time according to the config | ||||
| 		 * | ||||
| 		 * @param {object} config The config of the module | ||||
| 		 * @param {object} time time to format | ||||
| 		 * @returns {string} The formatted time string | ||||
| 		 */ | ||||
| 		function formatTime(config, time) { | ||||
| 			var formatString = hourSymbol + ":mm"; | ||||
| 			if (config.showPeriod && config.timeFormat !== 24) { | ||||
| 				formatString += config.showPeriodUpper ? "A" : "a"; | ||||
| 		if (this.config.displayType !== "analog" && this.config.showTime) { | ||||
| 			timeWrapper.innerHTML = timeString; | ||||
| 			secondsWrapper.innerHTML = now.format("ss"); | ||||
| 			if (this.config.showPeriodUpper) { | ||||
| 				periodWrapper.innerHTML = now.format("A"); | ||||
| 			} else { | ||||
| 				periodWrapper.innerHTML = now.format("a"); | ||||
| 			} | ||||
| 			return moment(time).format(formatString); | ||||
| 			if (this.config.displaySeconds) { | ||||
| 				timeWrapper.appendChild(secondsWrapper); | ||||
| 			} | ||||
| 			if (this.config.showPeriod && this.config.timeFormat !== 24) { | ||||
| 				timeWrapper.appendChild(periodWrapper); | ||||
| 			} | ||||
| 			digitalWrapper.appendChild(timeWrapper); | ||||
| 		} | ||||
|  | ||||
| 		/**************************************************************** | ||||
| 		 * Create wrappers for Sun Times, only if specified in config | ||||
| 		 */ | ||||
| 		if (this.config.showSunTimes) { | ||||
| 			const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon); | ||||
| 			const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset); | ||||
| 			var nextEvent; | ||||
| 			let nextEvent; | ||||
| 			if (now.isBefore(sunTimes.sunrise)) { | ||||
| 				nextEvent = sunTimes.sunrise; | ||||
| 			} else if (now.isBefore(sunTimes.sunset)) { | ||||
| @@ -180,25 +185,22 @@ Module.register("clock", { | ||||
| 				nextEvent = tomorrowSunTimes.sunrise; | ||||
| 			} | ||||
| 			const untilNextEvent = moment.duration(moment(nextEvent).diff(now)); | ||||
| 			const untilNextEventString = untilNextEvent.hours() + "h " + untilNextEvent.minutes() + "m"; | ||||
| 			const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`; | ||||
| 			sunWrapper.innerHTML = | ||||
| 				'<span class="' + | ||||
| 				(isVisible ? "bright" : "") + | ||||
| 				'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' + | ||||
| 				untilNextEventString + | ||||
| 				"</span>" + | ||||
| 				'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' + | ||||
| 				formatTime(this.config, sunTimes.sunrise) + | ||||
| 				"</span>" + | ||||
| 				'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' + | ||||
| 				formatTime(this.config, sunTimes.sunset) + | ||||
| 				"</span>"; | ||||
| 				`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>` + | ||||
| 				`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>` + | ||||
| 				`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunset)}</span>`; | ||||
| 			digitalWrapper.appendChild(sunWrapper); | ||||
| 		} | ||||
|  | ||||
| 		/**************************************************************** | ||||
| 		 * Create wrappers for Moon Times, only if specified in config | ||||
| 		 */ | ||||
| 		if (this.config.showMoonTimes) { | ||||
| 			const moonIllumination = SunCalc.getMoonIllumination(now.toDate()); | ||||
| 			const moonTimes = SunCalc.getMoonTimes(now, this.config.lat, this.config.lon); | ||||
| 			const moonRise = moonTimes.rise; | ||||
| 			var moonSet; | ||||
| 			let moonSet; | ||||
| 			if (moment(moonTimes.set).isAfter(moonTimes.rise)) { | ||||
| 				moonSet = moonTimes.set; | ||||
| 			} else { | ||||
| @@ -206,25 +208,22 @@ Module.register("clock", { | ||||
| 				moonSet = nextMoonTimes.set; | ||||
| 			} | ||||
| 			const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true; | ||||
| 			const illuminatedFractionString = Math.round(moonIllumination.fraction * 100) + "%"; | ||||
| 			const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`; | ||||
| 			moonWrapper.innerHTML = | ||||
| 				'<span class="' + | ||||
| 				(isVisible ? "bright" : "") + | ||||
| 				'"><i class="fa fa-moon-o" aria-hidden="true"></i> ' + | ||||
| 				illuminatedFractionString + | ||||
| 				"</span>" + | ||||
| 				'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' + | ||||
| 				(moonRise ? formatTime(this.config, moonRise) : "...") + | ||||
| 				"</span>" + | ||||
| 				'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' + | ||||
| 				(moonSet ? formatTime(this.config, moonSet) : "...") + | ||||
| 				"</span>"; | ||||
| 				`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-moon" aria-hidden="true"></i> ${illuminatedFractionString}</span>` + | ||||
| 				`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>` + | ||||
| 				`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${moonSet ? formatTime(this.config, moonSet) : "..."}</span>`; | ||||
| 			digitalWrapper.appendChild(moonWrapper); | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.showWeek) { | ||||
| 			weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() }); | ||||
| 			digitalWrapper.appendChild(weekWrapper); | ||||
| 		} | ||||
|  | ||||
| 		/**************************************************************** | ||||
| 		 * Create wrappers for ANALOG clock, only if specified in config | ||||
| 		 */ | ||||
|  | ||||
| 		if (this.config.displayType !== "digital") { | ||||
| 			// If it isn't 'digital', then an 'analog' clock was also requested | ||||
|  | ||||
| @@ -232,126 +231,73 @@ Module.register("clock", { | ||||
| 			if (this.config.timezone) { | ||||
| 				now.tz(this.config.timezone); | ||||
| 			} | ||||
| 			var second = now.seconds() * 6, | ||||
| 			const second = now.seconds() * 6, | ||||
| 				minute = now.minute() * 6 + second / 60, | ||||
| 				hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12; | ||||
|  | ||||
| 			// Create wrappers | ||||
| 			var clockCircle = document.createElement("div"); | ||||
| 			clockCircle.className = "clockCircle"; | ||||
| 			clockCircle.style.width = this.config.analogSize; | ||||
| 			clockCircle.style.height = this.config.analogSize; | ||||
| 			analogWrapper.style.width = this.config.analogSize; | ||||
| 			analogWrapper.style.height = this.config.analogSize; | ||||
|  | ||||
| 			if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") { | ||||
| 				clockCircle.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)"; | ||||
| 				clockCircle.style.backgroundSize = "100%"; | ||||
| 				analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`; | ||||
| 				analogWrapper.style.backgroundSize = "100%"; | ||||
|  | ||||
| 				// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611 | ||||
| 				// clockCircle.style.border = "1px solid black"; | ||||
| 				clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used | ||||
| 				// analogWrapper.style.border = "1px solid black"; | ||||
| 				analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used | ||||
| 			} else if (this.config.analogFace !== "none") { | ||||
| 				clockCircle.style.border = "2px solid white"; | ||||
| 				analogWrapper.style.border = "2px solid white"; | ||||
| 			} | ||||
| 			var clockFace = document.createElement("div"); | ||||
| 			clockFace.className = "clockFace"; | ||||
| 			const clockFace = document.createElement("div"); | ||||
| 			clockFace.className = "clock-face"; | ||||
|  | ||||
| 			var clockHour = document.createElement("div"); | ||||
| 			clockHour.id = "clockHour"; | ||||
| 			clockHour.style.transform = "rotate(" + hour + "deg)"; | ||||
| 			clockHour.className = "clockHour"; | ||||
| 			var clockMinute = document.createElement("div"); | ||||
| 			clockMinute.id = "clockMinute"; | ||||
| 			clockMinute.style.transform = "rotate(" + minute + "deg)"; | ||||
| 			clockMinute.className = "clockMinute"; | ||||
| 			const clockHour = document.createElement("div"); | ||||
| 			clockHour.id = "clock-hour"; | ||||
| 			clockHour.style.transform = `rotate(${hour}deg)`; | ||||
| 			clockHour.className = "clock-hour"; | ||||
| 			const clockMinute = document.createElement("div"); | ||||
| 			clockMinute.id = "clock-minute"; | ||||
| 			clockMinute.style.transform = `rotate(${minute}deg)`; | ||||
| 			clockMinute.className = "clock-minute"; | ||||
|  | ||||
| 			// Combine analog wrappers | ||||
| 			clockFace.appendChild(clockHour); | ||||
| 			clockFace.appendChild(clockMinute); | ||||
|  | ||||
| 			if (this.config.displaySeconds) { | ||||
| 				var clockSecond = document.createElement("div"); | ||||
| 				clockSecond.id = "clockSecond"; | ||||
| 				clockSecond.style.transform = "rotate(" + second + "deg)"; | ||||
| 				clockSecond.className = "clockSecond"; | ||||
| 				const clockSecond = document.createElement("div"); | ||||
| 				clockSecond.id = "clock-second"; | ||||
| 				clockSecond.style.transform = `rotate(${second}deg)`; | ||||
| 				clockSecond.className = "clock-second"; | ||||
| 				clockSecond.style.backgroundColor = this.config.secondsColor; | ||||
| 				clockFace.appendChild(clockSecond); | ||||
| 			} | ||||
| 			clockCircle.appendChild(clockFace); | ||||
| 			analogWrapper.appendChild(clockFace); | ||||
| 		} | ||||
|  | ||||
| 		/******************************************* | ||||
| 		 * Combine wrappers, check for .displayType | ||||
| 		 * Update placement, respect old analogShowDate even if it's not needed anymore | ||||
| 		 */ | ||||
|  | ||||
| 		if (this.config.displayType === "digital") { | ||||
| 			// Display only a digital clock | ||||
| 			wrapper.appendChild(dateWrapper); | ||||
| 			wrapper.appendChild(timeWrapper); | ||||
| 			wrapper.appendChild(sunWrapper); | ||||
| 			wrapper.appendChild(moonWrapper); | ||||
| 			wrapper.appendChild(weekWrapper); | ||||
| 		} else if (this.config.displayType === "analog") { | ||||
| 		if (this.config.displayType === "analog") { | ||||
| 			// Display only an analog clock | ||||
|  | ||||
| 			if (this.config.showWeek) { | ||||
| 				weekWrapper.style.paddingBottom = "15px"; | ||||
| 			} else { | ||||
| 				dateWrapper.style.paddingBottom = "15px"; | ||||
| 			} | ||||
|  | ||||
| 			if (this.config.analogShowDate === "top") { | ||||
| 			if (this.config.showDate) { | ||||
| 				// Add date to the analog clock | ||||
| 				dateWrapper.innerHTML = now.format(this.config.dateFormat); | ||||
| 				wrapper.appendChild(dateWrapper); | ||||
| 				wrapper.appendChild(weekWrapper); | ||||
| 				wrapper.appendChild(clockCircle); | ||||
| 			} else if (this.config.analogShowDate === "bottom") { | ||||
| 				wrapper.appendChild(clockCircle); | ||||
| 				wrapper.appendChild(dateWrapper); | ||||
| 				wrapper.appendChild(weekWrapper); | ||||
| 			} else { | ||||
| 				wrapper.appendChild(clockCircle); | ||||
| 			} | ||||
| 		} else { | ||||
| 			// Both clocks have been configured, check position | ||||
| 			var placement = this.config.analogPlacement; | ||||
|  | ||||
| 			var analogWrapper = document.createElement("div"); | ||||
| 			analogWrapper.id = "analog"; | ||||
| 			analogWrapper.style.cssFloat = "none"; | ||||
| 			analogWrapper.appendChild(clockCircle); | ||||
|  | ||||
| 			var digitalWrapper = document.createElement("div"); | ||||
| 			digitalWrapper.id = "digital"; | ||||
| 			digitalWrapper.style.cssFloat = "none"; | ||||
| 			digitalWrapper.appendChild(dateWrapper); | ||||
| 			digitalWrapper.appendChild(timeWrapper); | ||||
| 			digitalWrapper.appendChild(sunWrapper); | ||||
| 			digitalWrapper.appendChild(moonWrapper); | ||||
| 			digitalWrapper.appendChild(weekWrapper); | ||||
|  | ||||
| 			var appendClocks = function (condition, pos1, pos2) { | ||||
| 				var padding = [0, 0, 0, 0]; | ||||
| 				padding[placement === condition ? pos1 : pos2] = "20px"; | ||||
| 				analogWrapper.style.padding = padding.join(" "); | ||||
| 				if (placement === condition) { | ||||
| 					wrapper.appendChild(analogWrapper); | ||||
| 					wrapper.appendChild(digitalWrapper); | ||||
| 				} else { | ||||
| 					wrapper.appendChild(digitalWrapper); | ||||
| 					wrapper.appendChild(analogWrapper); | ||||
| 				} | ||||
| 			}; | ||||
|  | ||||
| 			if (placement === "left" || placement === "right") { | ||||
| 				digitalWrapper.style.display = "inline-block"; | ||||
| 				digitalWrapper.style.verticalAlign = "top"; | ||||
| 				analogWrapper.style.display = "inline-block"; | ||||
|  | ||||
| 				appendClocks("left", 1, 3); | ||||
| 			} else { | ||||
| 				digitalWrapper.style.textAlign = "center"; | ||||
|  | ||||
| 				appendClocks("top", 2, 0); | ||||
| 			if (this.config.analogShowDate === "bottom") { | ||||
| 				wrapper.classList.add("clock-grid-bottom"); | ||||
| 			} else if (this.config.analogShowDate === "top") { | ||||
| 				wrapper.classList.add("clock-grid-top"); | ||||
| 			} | ||||
| 			wrapper.appendChild(analogWrapper); | ||||
| 		} else if (this.config.displayType === "digital") { | ||||
| 			wrapper.appendChild(digitalWrapper); | ||||
| 		} else if (this.config.displayType === "both") { | ||||
| 			wrapper.classList.add(`clock-grid-${this.config.analogPlacement}`); | ||||
| 			wrapper.appendChild(analogWrapper); | ||||
| 			wrapper.appendChild(digitalWrapper); | ||||
| 		} | ||||
|  | ||||
| 		// Return the wrapper to the dom. | ||||
|   | ||||
| @@ -1,42 +1,63 @@ | ||||
| .clockCircle { | ||||
|   margin: 0 auto; | ||||
| .clock-grid { | ||||
|   display: inline-flex; | ||||
|   gap: 15px; | ||||
| } | ||||
|  | ||||
| .clock-grid-left { | ||||
|   flex-direction: row; | ||||
| } | ||||
|  | ||||
| .clock-grid-right { | ||||
|   flex-direction: row-reverse; | ||||
| } | ||||
|  | ||||
| .clock-grid-top { | ||||
|   flex-direction: column; | ||||
| } | ||||
|  | ||||
| .clock-grid-bottom { | ||||
|   flex-direction: column-reverse; | ||||
| } | ||||
|  | ||||
| .clock-circle { | ||||
|   place-self: center; | ||||
|   position: relative; | ||||
|   border-radius: 50%; | ||||
|   background-size: 100%; | ||||
| } | ||||
|  | ||||
| .clockFace { | ||||
| .clock-face { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| .clockFace::after { | ||||
| .clock-face::after { | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   width: 6px; | ||||
|   height: 6px; | ||||
|   margin: -3px 0 0 -3px; | ||||
|   background: white; | ||||
|   background: var(--color-text-bright); | ||||
|   border-radius: 3px; | ||||
|   content: ""; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .clockHour { | ||||
| .clock-hour { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */ | ||||
|   margin: -2px 0 -2px -25%; /* numbers must match negative length & thickness */ | ||||
|   padding: 2px 0 2px 25%; /* indicator length & thickness */ | ||||
|   background: white; | ||||
|   background: var(--color-text-bright); | ||||
|   transform-origin: 100% 50%; | ||||
|   border-radius: 3px 0 0 3px; | ||||
| } | ||||
|  | ||||
| .clockMinute { | ||||
| .clock-minute { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
|   position: absolute; | ||||
| @@ -44,12 +65,12 @@ | ||||
|   left: 50%; | ||||
|   margin: -35% -2px 0; /* numbers must match negative length & thickness */ | ||||
|   padding: 35% 2px 0; /* indicator length & thickness */ | ||||
|   background: white; | ||||
|   background: var(--color-text-bright); | ||||
|   transform-origin: 50% 100%; | ||||
|   border-radius: 3px 0 0 3px; | ||||
| } | ||||
|  | ||||
| .clockSecond { | ||||
| .clock-second { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
|   position: absolute; | ||||
| @@ -57,7 +78,7 @@ | ||||
|   left: 50%; | ||||
|   margin: -38% -1px 0 0; /* numbers must match negative length & thickness */ | ||||
|   padding: 38% 1px 0 0; /* indicator length & thickness */ | ||||
|   background: #888; | ||||
|   background: var(--color-text); | ||||
|   transform-origin: 50% 100%; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| # Module: Compliments | ||||
|  | ||||
| The `compliments` module is one of the default modules of the MagicMirror. | ||||
| The `compliments` module is one of the default modules of the MagicMirror². | ||||
| This module displays a random compliment. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/compliments.html). | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Module: Compliments | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
| @@ -21,8 +21,7 @@ Module.register("compliments", { | ||||
| 		morningEndTime: 12, | ||||
| 		afternoonStartTime: 12, | ||||
| 		afternoonEndTime: 17, | ||||
| 		random: true, | ||||
| 		mockDate: null | ||||
| 		random: true | ||||
| 	}, | ||||
| 	lastIndexUsed: -1, | ||||
| 	// Set currentweather from module | ||||
| @@ -34,42 +33,38 @@ Module.register("compliments", { | ||||
| 	}, | ||||
|  | ||||
| 	// Define start sequence. | ||||
| 	start: function () { | ||||
| 		Log.info("Starting module: " + this.name); | ||||
| 	start: async function () { | ||||
| 		Log.info(`Starting module: ${this.name}`); | ||||
|  | ||||
| 		this.lastComplimentIndex = -1; | ||||
|  | ||||
| 		var self = this; | ||||
| 		if (this.config.remoteFile !== null) { | ||||
| 			this.complimentFile(function (response) { | ||||
| 				self.config.compliments = JSON.parse(response); | ||||
| 				self.updateDom(); | ||||
| 			}); | ||||
| 			const response = await this.loadComplimentFile(); | ||||
| 			this.config.compliments = JSON.parse(response); | ||||
| 			this.updateDom(); | ||||
| 		} | ||||
|  | ||||
| 		// Schedule update timer. | ||||
| 		setInterval(function () { | ||||
| 			self.updateDom(self.config.fadeSpeed); | ||||
| 		setInterval(() => { | ||||
| 			this.updateDom(this.config.fadeSpeed); | ||||
| 		}, this.config.updateInterval); | ||||
| 	}, | ||||
|  | ||||
| 	/* randomIndex(compliments) | ||||
| 	/** | ||||
| 	 * Generate a random index for a list of compliments. | ||||
| 	 * | ||||
| 	 * argument compliments Array<String> - Array with compliments. | ||||
| 	 * | ||||
| 	 * return Number - Random index. | ||||
| 	 * @param {string[]} compliments Array with compliments. | ||||
| 	 * @returns {number} a random index of given array | ||||
| 	 */ | ||||
| 	randomIndex: function (compliments) { | ||||
| 		if (compliments.length === 1) { | ||||
| 			return 0; | ||||
| 		} | ||||
|  | ||||
| 		var generate = function () { | ||||
| 		const generate = function () { | ||||
| 			return Math.floor(Math.random() * compliments.length); | ||||
| 		}; | ||||
|  | ||||
| 		var complimentIndex = generate(); | ||||
| 		let complimentIndex = generate(); | ||||
|  | ||||
| 		while (complimentIndex === this.lastComplimentIndex) { | ||||
| 			complimentIndex = generate(); | ||||
| @@ -80,70 +75,62 @@ Module.register("compliments", { | ||||
| 		return complimentIndex; | ||||
| 	}, | ||||
|  | ||||
| 	/* complimentArray() | ||||
| 	/** | ||||
| 	 * Retrieve an array of compliments for the time of the day. | ||||
| 	 * | ||||
| 	 * return compliments Array<String> - Array with compliments for the time of the day. | ||||
| 	 * @returns {string[]} array with compliments for the time of the day. | ||||
| 	 */ | ||||
| 	complimentArray: function () { | ||||
| 		var hour = moment().hour(); | ||||
| 		var date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD"); | ||||
| 		var compliments; | ||||
| 		const hour = moment().hour(); | ||||
| 		const date = moment().format("YYYY-MM-DD"); | ||||
| 		let compliments = []; | ||||
|  | ||||
| 		// Add time of day compliments | ||||
| 		if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) { | ||||
| 			compliments = this.config.compliments.morning.slice(0); | ||||
| 			compliments = [...this.config.compliments.morning]; | ||||
| 		} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) { | ||||
| 			compliments = this.config.compliments.afternoon.slice(0); | ||||
| 			compliments = [...this.config.compliments.afternoon]; | ||||
| 		} else if (this.config.compliments.hasOwnProperty("evening")) { | ||||
| 			compliments = this.config.compliments.evening.slice(0); | ||||
| 		} | ||||
|  | ||||
| 		if (typeof compliments === "undefined") { | ||||
| 			compliments = new Array(); | ||||
| 			compliments = [...this.config.compliments.evening]; | ||||
| 		} | ||||
|  | ||||
| 		// Add compliments based on weather | ||||
| 		if (this.currentWeatherType in this.config.compliments) { | ||||
| 			compliments.push.apply(compliments, this.config.compliments[this.currentWeatherType]); | ||||
| 			Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]); | ||||
| 		} | ||||
|  | ||||
| 		compliments.push.apply(compliments, this.config.compliments.anytime); | ||||
| 		// Add compliments for anytime | ||||
| 		Array.prototype.push.apply(compliments, this.config.compliments.anytime); | ||||
|  | ||||
| 		for (var entry in this.config.compliments) { | ||||
| 		// Add compliments for special days | ||||
| 		for (let entry in this.config.compliments) { | ||||
| 			if (new RegExp(entry).test(date)) { | ||||
| 				compliments.push.apply(compliments, this.config.compliments[entry]); | ||||
| 				Array.prototype.push.apply(compliments, this.config.compliments[entry]); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return compliments; | ||||
| 	}, | ||||
|  | ||||
| 	/* complimentFile(callback) | ||||
| 	/** | ||||
| 	 * Retrieve a file from the local filesystem | ||||
| 	 * @returns {Promise} Resolved when the file is loaded | ||||
| 	 */ | ||||
| 	complimentFile: function (callback) { | ||||
| 		var xobj = new XMLHttpRequest(), | ||||
| 			isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0, | ||||
| 			path = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile); | ||||
| 		xobj.overrideMimeType("application/json"); | ||||
| 		xobj.open("GET", path, true); | ||||
| 		xobj.onreadystatechange = function () { | ||||
| 			if (xobj.readyState === 4 && xobj.status === 200) { | ||||
| 				callback(xobj.responseText); | ||||
| 			} | ||||
| 		}; | ||||
| 		xobj.send(null); | ||||
| 	loadComplimentFile: async function () { | ||||
| 		const isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0, | ||||
| 			url = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile); | ||||
| 		const response = await fetch(url); | ||||
| 		return await response.text(); | ||||
| 	}, | ||||
|  | ||||
| 	/* complimentArray() | ||||
| 	/** | ||||
| 	 * Retrieve a random compliment. | ||||
| 	 * | ||||
| 	 * return compliment string - A compliment. | ||||
| 	 * @returns {string} a compliment | ||||
| 	 */ | ||||
| 	randomCompliment: function () { | ||||
| 	getRandomCompliment: function () { | ||||
| 		// get the current time of day compliments list | ||||
| 		var compliments = this.complimentArray(); | ||||
| 		const compliments = this.complimentArray(); | ||||
| 		// variable for index to next message to display | ||||
| 		let index = 0; | ||||
| 		let index; | ||||
| 		// are we randomizing | ||||
| 		if (this.config.random) { | ||||
| 			// yes | ||||
| @@ -159,57 +146,36 @@ Module.register("compliments", { | ||||
|  | ||||
| 	// Override dom generator. | ||||
| 	getDom: function () { | ||||
| 		var wrapper = document.createElement("div"); | ||||
| 		const wrapper = document.createElement("div"); | ||||
| 		wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line"; | ||||
| 		// get the compliment text | ||||
| 		var complimentText = this.randomCompliment(); | ||||
| 		const complimentText = this.getRandomCompliment(); | ||||
| 		// split it into parts on newline text | ||||
| 		var parts = complimentText.split("\n"); | ||||
| 		// create a span to hold it all | ||||
| 		var compliment = document.createElement("span"); | ||||
| 		const parts = complimentText.split("\n"); | ||||
| 		// create a span to hold the compliment | ||||
| 		const compliment = document.createElement("span"); | ||||
| 		// process all the parts of the compliment text | ||||
| 		for (var part of parts) { | ||||
| 			// create a text element for each part | ||||
| 			compliment.appendChild(document.createTextNode(part)); | ||||
| 			// add a break ` | ||||
| 			compliment.appendChild(document.createElement("BR")); | ||||
| 		for (const part of parts) { | ||||
| 			if (part !== "") { | ||||
| 				// create a text element for each part | ||||
| 				compliment.appendChild(document.createTextNode(part)); | ||||
| 				// add a break | ||||
| 				compliment.appendChild(document.createElement("BR")); | ||||
| 			} | ||||
| 		} | ||||
| 		// only add compliment to wrapper if there is actual text in there | ||||
| 		if (compliment.children.length > 0) { | ||||
| 			// remove the last break | ||||
| 			compliment.lastElementChild.remove(); | ||||
| 			wrapper.appendChild(compliment); | ||||
| 		} | ||||
| 		// remove the last break | ||||
| 		compliment.lastElementChild.remove(); | ||||
| 		wrapper.appendChild(compliment); | ||||
|  | ||||
| 		return wrapper; | ||||
| 	}, | ||||
|  | ||||
| 	// From data currentweather set weather type | ||||
| 	setCurrentWeatherType: function (data) { | ||||
| 		var weatherIconTable = { | ||||
| 			"01d": "day_sunny", | ||||
| 			"02d": "day_cloudy", | ||||
| 			"03d": "cloudy", | ||||
| 			"04d": "cloudy_windy", | ||||
| 			"09d": "showers", | ||||
| 			"10d": "rain", | ||||
| 			"11d": "thunderstorm", | ||||
| 			"13d": "snow", | ||||
| 			"50d": "fog", | ||||
| 			"01n": "night_clear", | ||||
| 			"02n": "night_cloudy", | ||||
| 			"03n": "night_cloudy", | ||||
| 			"04n": "night_cloudy", | ||||
| 			"09n": "night_showers", | ||||
| 			"10n": "night_rain", | ||||
| 			"11n": "night_thunderstorm", | ||||
| 			"13n": "night_snow", | ||||
| 			"50n": "night_alt_cloudy_windy" | ||||
| 		}; | ||||
| 		this.currentWeatherType = weatherIconTable[data.weather[0].icon]; | ||||
| 	}, | ||||
|  | ||||
| 	// Override notification handler. | ||||
| 	notificationReceived: function (notification, payload, sender) { | ||||
| 		if (notification === "CURRENTWEATHER_DATA") { | ||||
| 			this.setCurrentWeatherType(payload.data); | ||||
| 		if (notification === "CURRENTWEATHER_TYPE") { | ||||
| 			this.currentWeatherType = payload.type; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| # Module: Current Weather | ||||
|  | ||||
| The `currentweather` module is one of the default modules of the MagicMirror. | ||||
| This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/currentweather.html). | ||||
| @@ -1,15 +0,0 @@ | ||||
| .currentweather .weathericon, | ||||
| .currentweather .fa-home { | ||||
|   font-size: 75%; | ||||
|   line-height: 65px; | ||||
|   display: inline-block; | ||||
|   transform: translate(0, -3px); | ||||
| } | ||||
|  | ||||
| .currentweather .humidityIcon { | ||||
|   padding-right: 4px; | ||||
| } | ||||
|  | ||||
| .currentweather .humidity-padding { | ||||
|   padding-bottom: 6px; | ||||
| } | ||||
| @@ -1,598 +0,0 @@ | ||||
| /* Magic Mirror | ||||
|  * Module: CurrentWeather | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| Module.register("currentweather", { | ||||
| 	// Default module config. | ||||
| 	defaults: { | ||||
| 		location: false, | ||||
| 		locationID: false, | ||||
| 		appid: "", | ||||
| 		units: config.units, | ||||
| 		updateInterval: 10 * 60 * 1000, // every 10 minutes | ||||
| 		animationSpeed: 1000, | ||||
| 		timeFormat: config.timeFormat, | ||||
| 		showPeriod: true, | ||||
| 		showPeriodUpper: false, | ||||
| 		showWindDirection: true, | ||||
| 		showWindDirectionAsArrow: false, | ||||
| 		useBeaufort: true, | ||||
| 		useKMPHwind: false, | ||||
| 		lang: config.language, | ||||
| 		decimalSymbol: ".", | ||||
| 		showHumidity: false, | ||||
| 		showSun: true, | ||||
| 		degreeLabel: false, | ||||
| 		showIndoorTemperature: false, | ||||
| 		showIndoorHumidity: false, | ||||
| 		showFeelsLike: true, | ||||
|  | ||||
| 		initialLoadDelay: 0, // 0 seconds delay | ||||
| 		retryDelay: 2500, | ||||
|  | ||||
| 		apiVersion: "2.5", | ||||
| 		apiBase: "https://api.openweathermap.org/data/", | ||||
| 		weatherEndpoint: "weather", | ||||
|  | ||||
| 		appendLocationNameToHeader: true, | ||||
| 		useLocationAsHeader: false, | ||||
|  | ||||
| 		calendarClass: "calendar", | ||||
| 		tableClass: "large", | ||||
|  | ||||
| 		onlyTemp: false, | ||||
| 		hideTemp: false, | ||||
| 		roundTemp: false, | ||||
|  | ||||
| 		iconTable: { | ||||
| 			"01d": "wi-day-sunny", | ||||
| 			"02d": "wi-day-cloudy", | ||||
| 			"03d": "wi-cloudy", | ||||
| 			"04d": "wi-cloudy-windy", | ||||
| 			"09d": "wi-showers", | ||||
| 			"10d": "wi-rain", | ||||
| 			"11d": "wi-thunderstorm", | ||||
| 			"13d": "wi-snow", | ||||
| 			"50d": "wi-fog", | ||||
| 			"01n": "wi-night-clear", | ||||
| 			"02n": "wi-night-cloudy", | ||||
| 			"03n": "wi-night-cloudy", | ||||
| 			"04n": "wi-night-cloudy", | ||||
| 			"09n": "wi-night-showers", | ||||
| 			"10n": "wi-night-rain", | ||||
| 			"11n": "wi-night-thunderstorm", | ||||
| 			"13n": "wi-night-snow", | ||||
| 			"50n": "wi-night-alt-cloudy-windy" | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	// create a variable for the first upcoming calendar event. Used if no location is specified. | ||||
| 	firstEvent: false, | ||||
|  | ||||
| 	// create a variable to hold the location name based on the API result. | ||||
| 	fetchedLocationName: "", | ||||
|  | ||||
| 	// Define required scripts. | ||||
| 	getScripts: function () { | ||||
| 		return ["moment.js"]; | ||||
| 	}, | ||||
|  | ||||
| 	// Define required scripts. | ||||
| 	getStyles: function () { | ||||
| 		return ["weather-icons.css", "currentweather.css"]; | ||||
| 	}, | ||||
|  | ||||
| 	// Define required translations. | ||||
| 	getTranslations: function () { | ||||
| 		// The translations for the default modules are defined in the core translation files. | ||||
| 		// Therefor we can just return false. Otherwise we should have returned a dictionary. | ||||
| 		// If you're trying to build your own module including translations, check out the documentation. | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	// Define start sequence. | ||||
| 	start: function () { | ||||
| 		Log.info("Starting module: " + this.name); | ||||
|  | ||||
| 		// Set locale. | ||||
| 		moment.locale(config.language); | ||||
|  | ||||
| 		this.windSpeed = null; | ||||
| 		this.windDirection = null; | ||||
| 		this.windDeg = null; | ||||
| 		this.sunriseSunsetTime = null; | ||||
| 		this.sunriseSunsetIcon = null; | ||||
| 		this.temperature = null; | ||||
| 		this.indoorTemperature = null; | ||||
| 		this.indoorHumidity = null; | ||||
| 		this.weatherType = null; | ||||
| 		this.feelsLike = null; | ||||
| 		this.loaded = false; | ||||
| 		this.scheduleUpdate(this.config.initialLoadDelay); | ||||
| 	}, | ||||
|  | ||||
| 	// add extra information of current weather | ||||
| 	// windDirection, humidity, sunrise and sunset | ||||
| 	addExtraInfoWeather: function (wrapper) { | ||||
| 		var small = document.createElement("div"); | ||||
| 		small.className = "normal medium"; | ||||
|  | ||||
| 		var windIcon = document.createElement("span"); | ||||
| 		windIcon.className = "wi wi-strong-wind dimmed"; | ||||
| 		small.appendChild(windIcon); | ||||
|  | ||||
| 		var windSpeed = document.createElement("span"); | ||||
| 		windSpeed.innerHTML = " " + this.windSpeed; | ||||
| 		small.appendChild(windSpeed); | ||||
|  | ||||
| 		if (this.config.showWindDirection) { | ||||
| 			var windDirection = document.createElement("sup"); | ||||
| 			if (this.config.showWindDirectionAsArrow) { | ||||
| 				if (this.windDeg !== null) { | ||||
| 					windDirection.innerHTML = '  <i class="fa fa-long-arrow-down" style="transform:rotate(' + this.windDeg + 'deg);"></i> '; | ||||
| 				} | ||||
| 			} else { | ||||
| 				windDirection.innerHTML = " " + this.translate(this.windDirection); | ||||
| 			} | ||||
| 			small.appendChild(windDirection); | ||||
| 		} | ||||
| 		var spacer = document.createElement("span"); | ||||
| 		spacer.innerHTML = " "; | ||||
| 		small.appendChild(spacer); | ||||
|  | ||||
| 		if (this.config.showHumidity) { | ||||
| 			var humidity = document.createElement("span"); | ||||
| 			humidity.innerHTML = this.humidity; | ||||
|  | ||||
| 			var supspacer = document.createElement("sup"); | ||||
| 			supspacer.innerHTML = " "; | ||||
|  | ||||
| 			var humidityIcon = document.createElement("sup"); | ||||
| 			humidityIcon.className = "wi wi-humidity humidityIcon"; | ||||
| 			humidityIcon.innerHTML = " "; | ||||
|  | ||||
| 			small.appendChild(humidity); | ||||
| 			small.appendChild(supspacer); | ||||
| 			small.appendChild(humidityIcon); | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.showSun) { | ||||
| 			var sunriseSunsetIcon = document.createElement("span"); | ||||
| 			sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon; | ||||
| 			small.appendChild(sunriseSunsetIcon); | ||||
|  | ||||
| 			var sunriseSunsetTime = document.createElement("span"); | ||||
| 			sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime; | ||||
| 			small.appendChild(sunriseSunsetTime); | ||||
| 		} | ||||
|  | ||||
| 		wrapper.appendChild(small); | ||||
| 	}, | ||||
|  | ||||
| 	// Override dom generator. | ||||
| 	getDom: function () { | ||||
| 		var wrapper = document.createElement("div"); | ||||
| 		wrapper.className = this.config.tableClass; | ||||
|  | ||||
| 		if (this.config.appid === "") { | ||||
| 			wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + "."; | ||||
| 			wrapper.className = "dimmed light small"; | ||||
| 			return wrapper; | ||||
| 		} | ||||
|  | ||||
| 		if (!this.loaded) { | ||||
| 			wrapper.innerHTML = this.translate("LOADING"); | ||||
| 			wrapper.className = "dimmed light small"; | ||||
| 			return wrapper; | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.onlyTemp === false) { | ||||
| 			this.addExtraInfoWeather(wrapper); | ||||
| 		} | ||||
|  | ||||
| 		var large = document.createElement("div"); | ||||
| 		large.className = "light"; | ||||
|  | ||||
| 		var degreeLabel = ""; | ||||
| 		if (this.config.units === "metric" || this.config.units === "imperial") { | ||||
| 			degreeLabel += "°"; | ||||
| 		} | ||||
| 		if (this.config.degreeLabel) { | ||||
| 			switch (this.config.units) { | ||||
| 				case "metric": | ||||
| 					degreeLabel += "C"; | ||||
| 					break; | ||||
| 				case "imperial": | ||||
| 					degreeLabel += "F"; | ||||
| 					break; | ||||
| 				case "default": | ||||
| 					degreeLabel += "K"; | ||||
| 					break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.decimalSymbol === "") { | ||||
| 			this.config.decimalSymbol = "."; | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.hideTemp === false) { | ||||
| 			var weatherIcon = document.createElement("span"); | ||||
| 			weatherIcon.className = "wi weathericon " + this.weatherType; | ||||
| 			large.appendChild(weatherIcon); | ||||
|  | ||||
| 			var temperature = document.createElement("span"); | ||||
| 			temperature.className = "bright"; | ||||
| 			temperature.innerHTML = " " + this.temperature.replace(".", this.config.decimalSymbol) + degreeLabel; | ||||
| 			large.appendChild(temperature); | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.showIndoorTemperature && this.indoorTemperature) { | ||||
| 			var indoorIcon = document.createElement("span"); | ||||
| 			indoorIcon.className = "fa fa-home"; | ||||
| 			large.appendChild(indoorIcon); | ||||
|  | ||||
| 			var indoorTemperatureElem = document.createElement("span"); | ||||
| 			indoorTemperatureElem.className = "bright"; | ||||
| 			indoorTemperatureElem.innerHTML = " " + this.indoorTemperature.replace(".", this.config.decimalSymbol) + degreeLabel; | ||||
| 			large.appendChild(indoorTemperatureElem); | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.showIndoorHumidity && this.indoorHumidity) { | ||||
| 			var indoorHumidityIcon = document.createElement("span"); | ||||
| 			indoorHumidityIcon.className = "fa fa-tint"; | ||||
| 			large.appendChild(indoorHumidityIcon); | ||||
|  | ||||
| 			var indoorHumidityElem = document.createElement("span"); | ||||
| 			indoorHumidityElem.className = "bright"; | ||||
| 			indoorHumidityElem.innerHTML = " " + this.indoorHumidity + "%"; | ||||
| 			large.appendChild(indoorHumidityElem); | ||||
| 		} | ||||
|  | ||||
| 		wrapper.appendChild(large); | ||||
|  | ||||
| 		if (this.config.showFeelsLike && this.config.onlyTemp === false) { | ||||
| 			var small = document.createElement("div"); | ||||
| 			small.className = "normal medium"; | ||||
|  | ||||
| 			var feelsLike = document.createElement("span"); | ||||
| 			feelsLike.className = "dimmed"; | ||||
| 			var feelsLikeHtml = this.translate("FEELS"); | ||||
| 			if (feelsLikeHtml.indexOf("{DEGREE}") > -1) { | ||||
| 				feelsLikeHtml = this.translate("FEELS", { | ||||
| 					DEGREE: this.feelsLike + degreeLabel | ||||
| 				}); | ||||
| 			} else feelsLikeHtml += " " + this.feelsLike + degreeLabel; | ||||
| 			feelsLike.innerHTML = feelsLikeHtml; | ||||
| 			small.appendChild(feelsLike); | ||||
|  | ||||
| 			wrapper.appendChild(small); | ||||
| 		} | ||||
|  | ||||
| 		return wrapper; | ||||
| 	}, | ||||
|  | ||||
| 	// Override getHeader method. | ||||
| 	getHeader: function () { | ||||
| 		if (this.config.useLocationAsHeader && this.config.location !== false) { | ||||
| 			return this.config.location; | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.appendLocationNameToHeader) { | ||||
| 			if (this.data.header) return this.data.header + " " + this.fetchedLocationName; | ||||
| 			else return this.fetchedLocationName; | ||||
| 		} | ||||
|  | ||||
| 		return this.data.header ? this.data.header : ""; | ||||
| 	}, | ||||
|  | ||||
| 	// Override notification handler. | ||||
| 	notificationReceived: function (notification, payload, sender) { | ||||
| 		if (notification === "DOM_OBJECTS_CREATED") { | ||||
| 			if (this.config.appendLocationNameToHeader) { | ||||
| 				this.hide(0, { lockString: this.identifier }); | ||||
| 			} | ||||
| 		} | ||||
| 		if (notification === "CALENDAR_EVENTS") { | ||||
| 			var senderClasses = sender.data.classes.toLowerCase().split(" "); | ||||
| 			if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) { | ||||
| 				this.firstEvent = false; | ||||
|  | ||||
| 				for (var e in payload) { | ||||
| 					var event = payload[e]; | ||||
| 					if (event.location || event.geo) { | ||||
| 						this.firstEvent = event; | ||||
| 						//Log.log("First upcoming event with location: ", event); | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if (notification === "INDOOR_TEMPERATURE") { | ||||
| 			this.indoorTemperature = this.roundValue(payload); | ||||
| 			this.updateDom(this.config.animationSpeed); | ||||
| 		} | ||||
| 		if (notification === "INDOOR_HUMIDITY") { | ||||
| 			this.indoorHumidity = this.roundValue(payload); | ||||
| 			this.updateDom(this.config.animationSpeed); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/* updateWeather(compliments) | ||||
| 	 * Requests new data from openweather.org. | ||||
| 	 * Calls processWeather on succesfull response. | ||||
| 	 */ | ||||
| 	updateWeather: function () { | ||||
| 		if (this.config.appid === "") { | ||||
| 			Log.error("CurrentWeather: APPID not set!"); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.weatherEndpoint + this.getParams(); | ||||
| 		var self = this; | ||||
| 		var retry = true; | ||||
|  | ||||
| 		var weatherRequest = new XMLHttpRequest(); | ||||
| 		weatherRequest.open("GET", url, true); | ||||
| 		weatherRequest.onreadystatechange = function () { | ||||
| 			if (this.readyState === 4) { | ||||
| 				if (this.status === 200) { | ||||
| 					self.processWeather(JSON.parse(this.response)); | ||||
| 				} else if (this.status === 401) { | ||||
| 					self.updateDom(self.config.animationSpeed); | ||||
|  | ||||
| 					Log.error(self.name + ": Incorrect APPID."); | ||||
| 					retry = true; | ||||
| 				} else { | ||||
| 					Log.error(self.name + ": Could not load weather."); | ||||
| 				} | ||||
|  | ||||
| 				if (retry) { | ||||
| 					self.scheduleUpdate(self.loaded ? -1 : self.config.retryDelay); | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 		weatherRequest.send(); | ||||
| 	}, | ||||
|  | ||||
| 	/* getParams(compliments) | ||||
| 	 * Generates an url with api parameters based on the config. | ||||
| 	 * | ||||
| 	 * return String - URL params. | ||||
| 	 */ | ||||
| 	getParams: function () { | ||||
| 		var params = "?"; | ||||
| 		if (this.config.locationID) { | ||||
| 			params += "id=" + this.config.locationID; | ||||
| 		} else if (this.config.location) { | ||||
| 			params += "q=" + this.config.location; | ||||
| 		} else if (this.firstEvent && this.firstEvent.geo) { | ||||
| 			params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon; | ||||
| 		} else if (this.firstEvent && this.firstEvent.location) { | ||||
| 			params += "q=" + this.firstEvent.location; | ||||
| 		} else { | ||||
| 			this.hide(this.config.animationSpeed, { lockString: this.identifier }); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		params += "&units=" + this.config.units; | ||||
| 		params += "&lang=" + this.config.lang; | ||||
| 		params += "&APPID=" + this.config.appid; | ||||
|  | ||||
| 		return params; | ||||
| 	}, | ||||
|  | ||||
| 	/* processWeather(data) | ||||
| 	 * Uses the received data to set the various values. | ||||
| 	 * | ||||
| 	 * argument data object - Weather information received form openweather.org. | ||||
| 	 */ | ||||
| 	processWeather: function (data) { | ||||
| 		if (!data || !data.main || typeof data.main.temp === "undefined") { | ||||
| 			// Did not receive usable new data. | ||||
| 			// Maybe this needs a better check? | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		this.humidity = parseFloat(data.main.humidity); | ||||
| 		this.temperature = this.roundValue(data.main.temp); | ||||
| 		this.fetchedLocationName = data.name; | ||||
| 		this.feelsLike = 0; | ||||
|  | ||||
| 		if (this.config.useBeaufort) { | ||||
| 			this.windSpeed = this.ms2Beaufort(this.roundValue(data.wind.speed)); | ||||
| 		} else if (this.config.useKMPHwind) { | ||||
| 			this.windSpeed = parseFloat((data.wind.speed * 60 * 60) / 1000).toFixed(0); | ||||
| 		} else { | ||||
| 			this.windSpeed = parseFloat(data.wind.speed).toFixed(0); | ||||
| 		} | ||||
|  | ||||
| 		// ONLY WORKS IF TEMP IN C // | ||||
| 		var windInMph = parseFloat(data.wind.speed * 2.23694); | ||||
|  | ||||
| 		var tempInF = 0; | ||||
| 		switch (this.config.units) { | ||||
| 			case "metric": | ||||
| 				tempInF = 1.8 * this.temperature + 32; | ||||
| 				break; | ||||
| 			case "imperial": | ||||
| 				tempInF = this.temperature; | ||||
| 				break; | ||||
| 			case "default": | ||||
| 				tempInF = 1.8 * (this.temperature - 273.15) + 32; | ||||
| 				break; | ||||
| 		} | ||||
|  | ||||
| 		if (windInMph > 3 && tempInF < 50) { | ||||
| 			// windchill | ||||
| 			var windChillInF = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16)); | ||||
| 			var windChillInC = (windChillInF - 32) * (5 / 9); | ||||
| 			// this.feelsLike = windChillInC.toFixed(0); | ||||
|  | ||||
| 			switch (this.config.units) { | ||||
| 				case "metric": | ||||
| 					this.feelsLike = windChillInC.toFixed(0); | ||||
| 					break; | ||||
| 				case "imperial": | ||||
| 					this.feelsLike = windChillInF.toFixed(0); | ||||
| 					break; | ||||
| 				case "default": | ||||
| 					this.feelsLike = (windChillInC + 273.15).toFixed(0); | ||||
| 					break; | ||||
| 			} | ||||
| 		} else if (tempInF > 80 && this.humidity > 40) { | ||||
| 			// heat index | ||||
| 			var Hindex = | ||||
| 				-42.379 + | ||||
| 				2.04901523 * tempInF + | ||||
| 				10.14333127 * this.humidity - | ||||
| 				0.22475541 * tempInF * this.humidity - | ||||
| 				6.83783 * Math.pow(10, -3) * tempInF * tempInF - | ||||
| 				5.481717 * Math.pow(10, -2) * this.humidity * this.humidity + | ||||
| 				1.22874 * Math.pow(10, -3) * tempInF * tempInF * this.humidity + | ||||
| 				8.5282 * Math.pow(10, -4) * tempInF * this.humidity * this.humidity - | ||||
| 				1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity; | ||||
|  | ||||
| 			switch (this.config.units) { | ||||
| 				case "metric": | ||||
| 					this.feelsLike = parseFloat((Hindex - 32) / 1.8).toFixed(0); | ||||
| 					break; | ||||
| 				case "imperial": | ||||
| 					this.feelsLike = Hindex.toFixed(0); | ||||
| 					break; | ||||
| 				case "default": | ||||
| 					var tc = parseFloat((Hindex - 32) / 1.8) + 273.15; | ||||
| 					this.feelsLike = tc.toFixed(0); | ||||
| 					break; | ||||
| 			} | ||||
| 		} else { | ||||
| 			this.feelsLike = parseFloat(this.temperature).toFixed(0); | ||||
| 		} | ||||
|  | ||||
| 		this.windDirection = this.deg2Cardinal(data.wind.deg); | ||||
| 		this.windDeg = data.wind.deg; | ||||
| 		this.weatherType = this.config.iconTable[data.weather[0].icon]; | ||||
|  | ||||
| 		var now = new Date(); | ||||
| 		var sunrise = new Date(data.sys.sunrise * 1000); | ||||
| 		var sunset = new Date(data.sys.sunset * 1000); | ||||
|  | ||||
| 		// The moment().format('h') method has a bug on the Raspberry Pi. | ||||
| 		// So we need to generate the timestring manually. | ||||
| 		// See issue: https://github.com/MichMich/MagicMirror/issues/181 | ||||
| 		var sunriseSunsetDateObject = sunrise < now && sunset > now ? sunset : sunrise; | ||||
| 		var timeString = moment(sunriseSunsetDateObject).format("HH:mm"); | ||||
| 		if (this.config.timeFormat !== 24) { | ||||
| 			//var hours = sunriseSunsetDateObject.getHours() % 12 || 12; | ||||
| 			if (this.config.showPeriod) { | ||||
| 				if (this.config.showPeriodUpper) { | ||||
| 					//timeString = hours + moment(sunriseSunsetDateObject).format(':mm A'); | ||||
| 					timeString = moment(sunriseSunsetDateObject).format("h:mm A"); | ||||
| 				} else { | ||||
| 					//timeString = hours + moment(sunriseSunsetDateObject).format(':mm a'); | ||||
| 					timeString = moment(sunriseSunsetDateObject).format("h:mm a"); | ||||
| 				} | ||||
| 			} else { | ||||
| 				//timeString = hours + moment(sunriseSunsetDateObject).format(':mm'); | ||||
| 				timeString = moment(sunriseSunsetDateObject).format("h:mm"); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		this.sunriseSunsetTime = timeString; | ||||
| 		this.sunriseSunsetIcon = sunrise < now && sunset > now ? "wi-sunset" : "wi-sunrise"; | ||||
|  | ||||
| 		this.show(this.config.animationSpeed, { lockString: this.identifier }); | ||||
| 		this.loaded = true; | ||||
| 		this.updateDom(this.config.animationSpeed); | ||||
| 		this.sendNotification("CURRENTWEATHER_DATA", { data: data }); | ||||
| 	}, | ||||
|  | ||||
| 	/* scheduleUpdate() | ||||
| 	 * Schedule next update. | ||||
| 	 * | ||||
| 	 * argument delay number - Milliseconds before next update. If empty, this.config.updateInterval is used. | ||||
| 	 */ | ||||
| 	scheduleUpdate: function (delay) { | ||||
| 		var nextLoad = this.config.updateInterval; | ||||
| 		if (typeof delay !== "undefined" && delay >= 0) { | ||||
| 			nextLoad = delay; | ||||
| 		} | ||||
|  | ||||
| 		var self = this; | ||||
| 		setTimeout(function () { | ||||
| 			self.updateWeather(); | ||||
| 		}, nextLoad); | ||||
| 	}, | ||||
|  | ||||
| 	/* ms2Beaufort(ms) | ||||
| 	 * Converts m2 to beaufort (windspeed). | ||||
| 	 * | ||||
| 	 * see: | ||||
| 	 *  https://www.spc.noaa.gov/faq/tornado/beaufort.html | ||||
| 	 *  https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale | ||||
| 	 * | ||||
| 	 * argument ms number - Windspeed in m/s. | ||||
| 	 * | ||||
| 	 * return number - Windspeed in beaufort. | ||||
| 	 */ | ||||
| 	ms2Beaufort: function (ms) { | ||||
| 		var kmh = (ms * 60 * 60) / 1000; | ||||
| 		var speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000]; | ||||
| 		for (var beaufort in speeds) { | ||||
| 			var speed = speeds[beaufort]; | ||||
| 			if (speed > kmh) { | ||||
| 				return beaufort; | ||||
| 			} | ||||
| 		} | ||||
| 		return 12; | ||||
| 	}, | ||||
|  | ||||
| 	deg2Cardinal: function (deg) { | ||||
| 		if (deg > 11.25 && deg <= 33.75) { | ||||
| 			return "NNE"; | ||||
| 		} else if (deg > 33.75 && deg <= 56.25) { | ||||
| 			return "NE"; | ||||
| 		} else if (deg > 56.25 && deg <= 78.75) { | ||||
| 			return "ENE"; | ||||
| 		} else if (deg > 78.75 && deg <= 101.25) { | ||||
| 			return "E"; | ||||
| 		} else if (deg > 101.25 && deg <= 123.75) { | ||||
| 			return "ESE"; | ||||
| 		} else if (deg > 123.75 && deg <= 146.25) { | ||||
| 			return "SE"; | ||||
| 		} else if (deg > 146.25 && deg <= 168.75) { | ||||
| 			return "SSE"; | ||||
| 		} else if (deg > 168.75 && deg <= 191.25) { | ||||
| 			return "S"; | ||||
| 		} else if (deg > 191.25 && deg <= 213.75) { | ||||
| 			return "SSW"; | ||||
| 		} else if (deg > 213.75 && deg <= 236.25) { | ||||
| 			return "SW"; | ||||
| 		} else if (deg > 236.25 && deg <= 258.75) { | ||||
| 			return "WSW"; | ||||
| 		} else if (deg > 258.75 && deg <= 281.25) { | ||||
| 			return "W"; | ||||
| 		} else if (deg > 281.25 && deg <= 303.75) { | ||||
| 			return "WNW"; | ||||
| 		} else if (deg > 303.75 && deg <= 326.25) { | ||||
| 			return "NW"; | ||||
| 		} else if (deg > 326.25 && deg <= 348.75) { | ||||
| 			return "NNW"; | ||||
| 		} else { | ||||
| 			return "N"; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/* function(temperature) | ||||
| 	 * Rounds a temperature to 1 decimal or integer (depending on config.roundTemp). | ||||
| 	 * | ||||
| 	 * argument temperature number - Temperature. | ||||
| 	 * | ||||
| 	 * return string - Rounded Temperature. | ||||
| 	 */ | ||||
| 	roundValue: function (temperature) { | ||||
| 		var decimals = this.config.roundTemp ? 0 : 1; | ||||
| 		return parseFloat(temperature).toFixed(decimals); | ||||
| 	} | ||||
| }); | ||||
| @@ -1,13 +1,10 @@ | ||||
| /* Magic Mirror | ||||
|  * Default Modules List | ||||
| /* MagicMirror² Default Modules List | ||||
|  * Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name. | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
|  | ||||
| // Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name. | ||||
|  | ||||
| var defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"]; | ||||
| const defaultModules = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather"]; | ||||
|  | ||||
| /*************** DO NOT EDIT THE LINE BELOW ***************/ | ||||
| if (typeof module !== "undefined") { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # Module: Hello World | ||||
|  | ||||
| The `helloworld` module is one of the default modules of the MagicMirror. It is a simple way to display a static text on the mirror. | ||||
| The `helloworld` module is one of the default modules of the MagicMirror². It is a simple way to display a static text on the mirror. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/helloworld.html). | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Module: HelloWorld | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|   | ||||
| @@ -2,4 +2,4 @@ | ||||
| 	Use ` | safe` to allow html tages within the text string. | ||||
| 	https://mozilla.github.io/nunjucks/templating.html#autoescaping | ||||
| --> | ||||
| <div>{{text | safe}}</div> | ||||
| <div>{{ text | safe }}</div> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| # Module: News Feed | ||||
|  | ||||
| The `newsfeed` module is one of the default modules of the MagicMirror. | ||||
| The `newsfeed` module is one of the default modules of the MagicMirror². | ||||
| This module displays news headlines based on an RSS feed. Scrolling through news headlines happens time-based (`updateInterval`), but can also be controlled by sending news feed specific notifications to the module. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/newsfeed.html). | ||||
|   | ||||
							
								
								
									
										3
									
								
								modules/default/newsfeed/fullarticle.njk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								modules/default/newsfeed/fullarticle.njk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| <div> | ||||
|     <iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe> | ||||
| </div> | ||||
							
								
								
									
										24
									
								
								modules/default/newsfeed/newsfeed.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								modules/default/newsfeed/newsfeed.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| iframe.newsfeed-fullarticle { | ||||
|   width: 100vw; | ||||
|  | ||||
|   /* very large height value to allow scrolling */ | ||||
|   height: 3000px; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   border: none; | ||||
|   z-index: 1; | ||||
| } | ||||
|  | ||||
| .region.bottom.bar.newsfeed-fullarticle { | ||||
|   bottom: inherit; | ||||
|   top: -90px; | ||||
| } | ||||
|  | ||||
| .newsfeed-list { | ||||
|   list-style: none; | ||||
| } | ||||
|  | ||||
| .newsfeed-list li { | ||||
|   text-align: justify; | ||||
|   margin-bottom: 0.5em; | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Module: NewsFeed | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
| @@ -14,11 +14,13 @@ Module.register("newsfeed", { | ||||
| 				encoding: "UTF-8" //ISO-8859-1 | ||||
| 			} | ||||
| 		], | ||||
| 		showAsList: false, | ||||
| 		showSourceTitle: true, | ||||
| 		showPublishDate: true, | ||||
| 		broadcastNewsFeeds: true, | ||||
| 		broadcastNewsUpdates: true, | ||||
| 		showDescription: false, | ||||
| 		showTitleAsUrl: false, | ||||
| 		wrapTitle: true, | ||||
| 		wrapDescription: true, | ||||
| 		truncDescription: true, | ||||
| @@ -36,7 +38,16 @@ Module.register("newsfeed", { | ||||
| 		endTags: [], | ||||
| 		prohibitedWords: [], | ||||
| 		scrollLength: 500, | ||||
| 		logFeedWarnings: false | ||||
| 		logFeedWarnings: false, | ||||
| 		dangerouslyDisableAutoEscaping: false | ||||
| 	}, | ||||
|  | ||||
| 	getUrlPrefix: function (item) { | ||||
| 		if (item.useCorsProxy) { | ||||
| 			return `${location.protocol}//${location.host}/cors?url=`; | ||||
| 		} else { | ||||
| 			return ""; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	// Define required scripts. | ||||
| @@ -44,6 +55,11 @@ Module.register("newsfeed", { | ||||
| 		return ["moment.js"]; | ||||
| 	}, | ||||
|  | ||||
| 	//Define required styles. | ||||
| 	getStyles: function () { | ||||
| 		return ["newsfeed.css"]; | ||||
| 	}, | ||||
|  | ||||
| 	// Define required translations. | ||||
| 	getTranslations: function () { | ||||
| 		// The translations for the default modules are defined in the core translation files. | ||||
| @@ -54,13 +70,14 @@ Module.register("newsfeed", { | ||||
|  | ||||
| 	// Define start sequence. | ||||
| 	start: function () { | ||||
| 		Log.info("Starting module: " + this.name); | ||||
| 		Log.info(`Starting module: ${this.name}`); | ||||
|  | ||||
| 		// Set locale. | ||||
| 		moment.locale(config.language); | ||||
|  | ||||
| 		this.newsItems = []; | ||||
| 		this.loaded = false; | ||||
| 		this.error = null; | ||||
| 		this.activeItem = 0; | ||||
| 		this.scrollPosition = 0; | ||||
|  | ||||
| @@ -75,142 +92,84 @@ Module.register("newsfeed", { | ||||
| 			this.generateFeed(payload); | ||||
|  | ||||
| 			if (!this.loaded) { | ||||
| 				if (this.config.hideLoading) { | ||||
| 					this.show(); | ||||
| 				} | ||||
| 				this.scheduleUpdateInterval(); | ||||
| 			} | ||||
|  | ||||
| 			this.loaded = true; | ||||
| 			this.error = null; | ||||
| 		} else if (notification === "NEWSFEED_ERROR") { | ||||
| 			this.error = this.translate(payload.error_type); | ||||
| 			this.scheduleUpdateInterval(); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	// Override dom generator. | ||||
| 	getDom: function () { | ||||
| 		const wrapper = document.createElement("div"); | ||||
|  | ||||
| 	//Override fetching of template name | ||||
| 	getTemplate: function () { | ||||
| 		if (this.config.feedUrl) { | ||||
| 			wrapper.className = "small bright"; | ||||
| 			wrapper.innerHTML = this.translate("MODULE_CONFIG_CHANGED", { MODULE_NAME: "Newsfeed" }); | ||||
| 			return wrapper; | ||||
| 			return "oldconfig.njk"; | ||||
| 		} else if (this.config.showFullArticle) { | ||||
| 			return "fullarticle.njk"; | ||||
| 		} | ||||
| 		return "newsfeed.njk"; | ||||
| 	}, | ||||
|  | ||||
| 	//Override template data and return whats used for the current template | ||||
| 	getTemplateData: function () { | ||||
| 		// this.config.showFullArticle is a run-time configuration, triggered by optional notifications | ||||
| 		if (this.config.showFullArticle) { | ||||
| 			return { | ||||
| 				url: this.getActiveItemURL() | ||||
| 			}; | ||||
| 		} | ||||
| 		if (this.error) { | ||||
| 			return { | ||||
| 				error: this.error | ||||
| 			}; | ||||
| 		} | ||||
| 		if (this.newsItems.length === 0) { | ||||
| 			return { | ||||
| 				empty: true | ||||
| 			}; | ||||
| 		} | ||||
| 		if (this.activeItem >= this.newsItems.length) { | ||||
| 			this.activeItem = 0; | ||||
| 		} | ||||
|  | ||||
| 		if (this.newsItems.length > 0) { | ||||
| 			// this.config.showFullArticle is a run-time configuration, triggered by optional notifications | ||||
| 			if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) { | ||||
| 				const sourceAndTimestamp = document.createElement("div"); | ||||
| 				sourceAndTimestamp.className = "newsfeed-source light small dimmed"; | ||||
| 		const item = this.newsItems[this.activeItem]; | ||||
| 		const items = this.newsItems.map(function (item) { | ||||
| 			item.publishDate = moment(new Date(item.pubdate)).fromNow(); | ||||
| 			return item; | ||||
| 		}); | ||||
|  | ||||
| 				if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") { | ||||
| 					sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle; | ||||
| 				} | ||||
| 				if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" && this.config.showPublishDate) { | ||||
| 					sourceAndTimestamp.innerHTML += ", "; | ||||
| 				} | ||||
| 				if (this.config.showPublishDate) { | ||||
| 					sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow(); | ||||
| 				} | ||||
| 				if ((this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") || this.config.showPublishDate) { | ||||
| 					sourceAndTimestamp.innerHTML += ":"; | ||||
| 				} | ||||
|  | ||||
| 				wrapper.appendChild(sourceAndTimestamp); | ||||
| 			} | ||||
|  | ||||
| 			//Remove selected tags from the beginning of rss feed items (title or description) | ||||
|  | ||||
| 			if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") { | ||||
| 				for (let f = 0; f < this.config.startTags.length; f++) { | ||||
| 					if (this.newsItems[this.activeItem].title.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) { | ||||
| 						this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].title.length); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") { | ||||
| 				if (this.isShowingDescription) { | ||||
| 					for (let f = 0; f < this.config.startTags.length; f++) { | ||||
| 						if (this.newsItems[this.activeItem].description.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) { | ||||
| 							this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].description.length); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			//Remove selected tags from the end of rss feed items (title or description) | ||||
|  | ||||
| 			if (this.config.removeEndTags) { | ||||
| 				for (let f = 0; f < this.config.endTags.length; f++) { | ||||
| 					if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length) === this.config.endTags[f]) { | ||||
| 						this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(0, -this.config.endTags[f].length); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if (this.isShowingDescription) { | ||||
| 					for (let f = 0; f < this.config.endTags.length; f++) { | ||||
| 						if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length) === this.config.endTags[f]) { | ||||
| 							this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(0, -this.config.endTags[f].length); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (!this.config.showFullArticle) { | ||||
| 				const title = document.createElement("div"); | ||||
| 				title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : ""); | ||||
| 				title.innerHTML = this.newsItems[this.activeItem].title; | ||||
| 				wrapper.appendChild(title); | ||||
| 			} | ||||
|  | ||||
| 			if (this.isShowingDescription) { | ||||
| 				const description = document.createElement("div"); | ||||
| 				description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : ""); | ||||
| 				const txtDesc = this.newsItems[this.activeItem].description; | ||||
| 				description.innerHTML = this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc; | ||||
| 				wrapper.appendChild(description); | ||||
| 			} | ||||
|  | ||||
| 			if (this.config.showFullArticle) { | ||||
| 				const fullArticle = document.createElement("iframe"); | ||||
| 				fullArticle.className = ""; | ||||
| 				fullArticle.style.width = "100vw"; | ||||
| 				// very large height value to allow scrolling | ||||
| 				fullArticle.height = "3000"; | ||||
| 				fullArticle.style.height = "3000"; | ||||
| 				fullArticle.style.top = "0"; | ||||
| 				fullArticle.style.left = "0"; | ||||
| 				fullArticle.style.border = "none"; | ||||
| 				fullArticle.src = this.getActiveItemURL(); | ||||
| 				fullArticle.style.zIndex = 1; | ||||
| 				wrapper.appendChild(fullArticle); | ||||
| 			} | ||||
|  | ||||
| 			if (this.config.hideLoading) { | ||||
| 				this.show(); | ||||
| 			} | ||||
| 		} else { | ||||
| 			if (this.config.hideLoading) { | ||||
| 				this.hide(); | ||||
| 			} else { | ||||
| 				wrapper.innerHTML = this.translate("LOADING"); | ||||
| 				wrapper.className = "small dimmed"; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return wrapper; | ||||
| 		return { | ||||
| 			loaded: true, | ||||
| 			config: this.config, | ||||
| 			sourceTitle: item.sourceTitle, | ||||
| 			publishDate: moment(new Date(item.pubdate)).fromNow(), | ||||
| 			title: item.title, | ||||
| 			url: this.getUrlPrefix(item) + item.url, | ||||
| 			description: item.description, | ||||
| 			items: items | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	getActiveItemURL: function () { | ||||
| 		return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href; | ||||
| 		const item = this.newsItems[this.activeItem]; | ||||
| 		if (item) { | ||||
| 			return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href; | ||||
| 		} else { | ||||
| 			return ""; | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Registers the feeds to be used by the backend. | ||||
| 	 */ | ||||
| 	registerFeeds: function () { | ||||
| 		for (var f in this.config.feeds) { | ||||
| 			var feed = this.config.feeds[f]; | ||||
| 		for (let feed of this.config.feeds) { | ||||
| 			this.sendSocketNotification("ADD_FEED", { | ||||
| 				feed: feed, | ||||
| 				config: this.config | ||||
| @@ -220,16 +179,14 @@ Module.register("newsfeed", { | ||||
|  | ||||
| 	/** | ||||
| 	 * Generate an ordered list of items for this configured module. | ||||
| 	 * | ||||
| 	 * @param {object} feeds An object with feeds returned by the node helper. | ||||
| 	 */ | ||||
| 	generateFeed: function (feeds) { | ||||
| 		var newsItems = []; | ||||
| 		for (var feed in feeds) { | ||||
| 			var feedItems = feeds[feed]; | ||||
| 		let newsItems = []; | ||||
| 		for (let feed in feeds) { | ||||
| 			const feedItems = feeds[feed]; | ||||
| 			if (this.subscribedToFeed(feed)) { | ||||
| 				for (var i in feedItems) { | ||||
| 					var item = feedItems[i]; | ||||
| 				for (let item of feedItems) { | ||||
| 					item.sourceTitle = this.titleForFeed(feed); | ||||
| 					if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) { | ||||
| 						newsItems.push(item); | ||||
| @@ -238,27 +195,65 @@ Module.register("newsfeed", { | ||||
| 			} | ||||
| 		} | ||||
| 		newsItems.sort(function (a, b) { | ||||
| 			var dateA = new Date(a.pubdate); | ||||
| 			var dateB = new Date(b.pubdate); | ||||
| 			const dateA = new Date(a.pubdate); | ||||
| 			const dateB = new Date(b.pubdate); | ||||
| 			return dateB - dateA; | ||||
| 		}); | ||||
|  | ||||
| 		if (this.config.maxNewsItems > 0) { | ||||
| 			newsItems = newsItems.slice(0, this.config.maxNewsItems); | ||||
| 		} | ||||
|  | ||||
| 		if (this.config.prohibitedWords.length > 0) { | ||||
| 			newsItems = newsItems.filter(function (value) { | ||||
| 				for (var i = 0; i < this.config.prohibitedWords.length; i++) { | ||||
| 					if (value["title"].toLowerCase().indexOf(this.config.prohibitedWords[i].toLowerCase()) > -1) { | ||||
| 			newsItems = newsItems.filter(function (item) { | ||||
| 				for (let word of this.config.prohibitedWords) { | ||||
| 					if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) { | ||||
| 						return false; | ||||
| 					} | ||||
| 				} | ||||
| 				return true; | ||||
| 			}, this); | ||||
| 		} | ||||
| 		newsItems.forEach((item) => { | ||||
| 			//Remove selected tags from the beginning of rss feed items (title or description) | ||||
| 			if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") { | ||||
| 				for (let startTag of this.config.startTags) { | ||||
| 					if (item.title.slice(0, startTag.length) === startTag) { | ||||
| 						item.title = item.title.slice(startTag.length, item.title.length); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") { | ||||
| 				if (this.isShowingDescription) { | ||||
| 					for (let startTag of this.config.startTags) { | ||||
| 						if (item.description.slice(0, startTag.length) === startTag) { | ||||
| 							item.description = item.description.slice(startTag.length, item.description.length); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			//Remove selected tags from the end of rss feed items (title or description) | ||||
| 			if (this.config.removeEndTags) { | ||||
| 				for (let endTag of this.config.endTags) { | ||||
| 					if (item.title.slice(-endTag.length) === endTag) { | ||||
| 						item.title = item.title.slice(0, -endTag.length); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if (this.isShowingDescription) { | ||||
| 					for (let endTag of this.config.endTags) { | ||||
| 						if (item.description.slice(-endTag.length) === endTag) { | ||||
| 							item.description = item.description.slice(0, -endTag.length); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		// get updated news items and broadcast them | ||||
| 		var updatedItems = []; | ||||
| 		const updatedItems = []; | ||||
| 		newsItems.forEach((value) => { | ||||
| 			if (this.newsItems.findIndex((value1) => value1 === value) === -1) { | ||||
| 				// Add item to updated items list | ||||
| @@ -276,13 +271,11 @@ Module.register("newsfeed", { | ||||
|  | ||||
| 	/** | ||||
| 	 * Check if this module is configured to show this feed. | ||||
| 	 * | ||||
| 	 * @param {string} feedUrl Url of the feed to check. | ||||
| 	 * @returns {boolean} True if it is subscribed, false otherwise | ||||
| 	 */ | ||||
| 	subscribedToFeed: function (feedUrl) { | ||||
| 		for (var f in this.config.feeds) { | ||||
| 			var feed = this.config.feeds[f]; | ||||
| 		for (let feed of this.config.feeds) { | ||||
| 			if (feed.url === feedUrl) { | ||||
| 				return true; | ||||
| 			} | ||||
| @@ -292,13 +285,11 @@ Module.register("newsfeed", { | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns title for the specific feed url. | ||||
| 	 * | ||||
| 	 * @param {string} feedUrl Url of the feed | ||||
| 	 * @returns {string} The title of the feed | ||||
| 	 */ | ||||
| 	titleForFeed: function (feedUrl) { | ||||
| 		for (var f in this.config.feeds) { | ||||
| 			var feed = this.config.feeds[f]; | ||||
| 		for (let feed of this.config.feeds) { | ||||
| 			if (feed.url === feedUrl) { | ||||
| 				return feed.title || ""; | ||||
| 			} | ||||
| @@ -310,22 +301,23 @@ Module.register("newsfeed", { | ||||
| 	 * Schedule visual update. | ||||
| 	 */ | ||||
| 	scheduleUpdateInterval: function () { | ||||
| 		var self = this; | ||||
|  | ||||
| 		self.updateDom(self.config.animationSpeed); | ||||
| 		this.updateDom(this.config.animationSpeed); | ||||
|  | ||||
| 		// Broadcast NewsFeed if needed | ||||
| 		if (self.config.broadcastNewsFeeds) { | ||||
| 			self.sendNotification("NEWS_FEED", { items: self.newsItems }); | ||||
| 		if (this.config.broadcastNewsFeeds) { | ||||
| 			this.sendNotification("NEWS_FEED", { items: this.newsItems }); | ||||
| 		} | ||||
|  | ||||
| 		this.timer = setInterval(function () { | ||||
| 			self.activeItem++; | ||||
| 			self.updateDom(self.config.animationSpeed); | ||||
| 		// #2638 Clear timer if it already exists | ||||
| 		if (this.timer) clearInterval(this.timer); | ||||
|  | ||||
| 		this.timer = setInterval(() => { | ||||
| 			this.activeItem++; | ||||
| 			this.updateDom(this.config.animationSpeed); | ||||
|  | ||||
| 			// Broadcast NewsFeed if needed | ||||
| 			if (self.config.broadcastNewsFeeds) { | ||||
| 				self.sendNotification("NEWS_FEED", { items: self.newsItems }); | ||||
| 			if (this.config.broadcastNewsFeeds) { | ||||
| 				this.sendNotification("NEWS_FEED", { items: this.newsItems }); | ||||
| 			} | ||||
| 		}, this.config.updateInterval); | ||||
| 	}, | ||||
| @@ -335,8 +327,7 @@ Module.register("newsfeed", { | ||||
| 		this.config.showFullArticle = false; | ||||
| 		this.scrollPosition = 0; | ||||
| 		// reset bottom bar alignment | ||||
| 		document.getElementsByClassName("region bottom bar")[0].style.bottom = "0"; | ||||
| 		document.getElementsByClassName("region bottom bar")[0].style.top = "inherit"; | ||||
| 		document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle"); | ||||
| 		if (!this.timer) { | ||||
| 			this.scheduleUpdateInterval(); | ||||
| 		} | ||||
| @@ -344,13 +335,15 @@ Module.register("newsfeed", { | ||||
|  | ||||
| 	notificationReceived: function (notification, payload, sender) { | ||||
| 		const before = this.activeItem; | ||||
| 		if (notification === "ARTICLE_NEXT") { | ||||
| 		if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) { | ||||
| 			this.hide(); | ||||
| 		} else if (notification === "ARTICLE_NEXT") { | ||||
| 			this.activeItem++; | ||||
| 			if (this.activeItem >= this.newsItems.length) { | ||||
| 				this.activeItem = 0; | ||||
| 			} | ||||
| 			this.resetDescrOrFullArticleAndTimer(); | ||||
| 			Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")"); | ||||
| 			Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`); | ||||
| 			this.updateDom(100); | ||||
| 		} else if (notification === "ARTICLE_PREVIOUS") { | ||||
| 			this.activeItem--; | ||||
| @@ -358,7 +351,7 @@ Module.register("newsfeed", { | ||||
| 				this.activeItem = this.newsItems.length - 1; | ||||
| 			} | ||||
| 			this.resetDescrOrFullArticleAndTimer(); | ||||
| 			Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")"); | ||||
| 			Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`); | ||||
| 			this.updateDom(100); | ||||
| 		} | ||||
| 		// if "more details" is received the first time: show article summary, on second time show full article | ||||
| @@ -367,8 +360,8 @@ Module.register("newsfeed", { | ||||
| 			if (this.config.showFullArticle === true) { | ||||
| 				this.scrollPosition += this.config.scrollLength; | ||||
| 				window.scrollTo(0, this.scrollPosition); | ||||
| 				Log.debug(this.name + " - scrolling down"); | ||||
| 				Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength); | ||||
| 				Log.debug(`${this.name} - scrolling down`); | ||||
| 				Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`); | ||||
| 			} else { | ||||
| 				this.showFullArticle(); | ||||
| 			} | ||||
| @@ -376,12 +369,12 @@ Module.register("newsfeed", { | ||||
| 			if (this.config.showFullArticle === true) { | ||||
| 				this.scrollPosition -= this.config.scrollLength; | ||||
| 				window.scrollTo(0, this.scrollPosition); | ||||
| 				Log.debug(this.name + " - scrolling up"); | ||||
| 				Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength); | ||||
| 				Log.debug(`${this.name} - scrolling up`); | ||||
| 				Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`); | ||||
| 			} | ||||
| 		} else if (notification === "ARTICLE_LESS_DETAILS") { | ||||
| 			this.resetDescrOrFullArticleAndTimer(); | ||||
| 			Log.debug(this.name + " - showing only article titles again"); | ||||
| 			Log.debug(`${this.name} - showing only article titles again`); | ||||
| 			this.updateDom(100); | ||||
| 		} else if (notification === "ARTICLE_TOGGLE_FULL") { | ||||
| 			if (this.config.showFullArticle) { | ||||
| @@ -406,12 +399,11 @@ Module.register("newsfeed", { | ||||
| 		this.config.showFullArticle = !this.isShowingDescription; | ||||
| 		// make bottom bar align to top to allow scrolling | ||||
| 		if (this.config.showFullArticle === true) { | ||||
| 			document.getElementsByClassName("region bottom bar")[0].style.bottom = "inherit"; | ||||
| 			document.getElementsByClassName("region bottom bar")[0].style.top = "-90px"; | ||||
| 			document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle"); | ||||
| 		} | ||||
| 		clearInterval(this.timer); | ||||
| 		this.timer = null; | ||||
| 		Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article"); | ||||
| 		Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`); | ||||
| 		this.updateDom(100); | ||||
| 	} | ||||
| }); | ||||
|   | ||||
							
								
								
									
										89
									
								
								modules/default/newsfeed/newsfeed.njk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								modules/default/newsfeed/newsfeed.njk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| {% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %} | ||||
|     {% if dangerouslyDisableAutoEscaping -%} | ||||
|         {{ text | safe }} | ||||
|     {%- else -%} | ||||
|         {{ text }} | ||||
|     {%- endif %} | ||||
| {% endmacro %} | ||||
| {% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %} | ||||
|     {% if dangerouslyDisableAutoEscaping %} | ||||
|         {% if showTitleAsUrl %} | ||||
|             <a href="{{ url }}" | ||||
|                style="text-decoration:none; | ||||
|                       color:#ffffff" | ||||
|                target="_blank">{{ title | safe }}</a> | ||||
|         {% else %} | ||||
|             {{ title | safe }} | ||||
|         {% endif %} | ||||
|     {% else %} | ||||
|         {% if showTitleAsUrl %} | ||||
|             <a href="{{ url }}" | ||||
|                style="text-decoration:none; | ||||
|                       color:#ffffff" | ||||
|                target="_blank">{{ title }}</a> | ||||
|         {% else %} | ||||
|             {{ title }} | ||||
|         {% endif %} | ||||
|     {% endif %} | ||||
| {% endmacro %} | ||||
| {% if loaded %} | ||||
|     {% if config.showAsList %} | ||||
|         <ul class="newsfeed-list"> | ||||
|             {% for item in items %} | ||||
|                 <li> | ||||
|                     {% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %} | ||||
|                         <div class="newsfeed-source light small dimmed"> | ||||
|                             {% if item.sourceTitle and config.showSourceTitle %} | ||||
|                                 {{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %} | ||||
|                             {% endif %} | ||||
|                             {% if config.showPublishDate %}{{ item.publishDate }}:{% endif %} | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                     <div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}"> | ||||
|                         {{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }} | ||||
|                     </div> | ||||
|                     {% if config.showDescription %} | ||||
|                         <div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}"> | ||||
|                             {% if config.truncDescription %} | ||||
|                                 {{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }} | ||||
|                             {% else %} | ||||
|                                 {{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }} | ||||
|                             {% endif %} | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                 </li> | ||||
|             {% endfor %} | ||||
|         </ul> | ||||
|     {% else %} | ||||
|         <div> | ||||
|             {% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %} | ||||
|                 <div class="newsfeed-source light small dimmed"> | ||||
|                     {% if sourceTitle and config.showSourceTitle %} | ||||
|                         {{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %} | ||||
|                     {% endif %} | ||||
|                     {% if config.showPublishDate %}{{ publishDate }}:{% endif %} | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|             <div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}"> | ||||
|                 {{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }} | ||||
|             </div> | ||||
|             {% if config.showDescription %} | ||||
|                 <div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}"> | ||||
|                     {% if config.truncDescription %} | ||||
|                         {{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }} | ||||
|                     {% else %} | ||||
|                         {{ escapeText(description, config.dangerouslyDisableAutoEscaping) }} | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|     {% endif %} | ||||
| {% elseif empty %} | ||||
|     <div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div> | ||||
| {% elseif error %} | ||||
|     <div class="small dimmed"> | ||||
|         {{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }} | ||||
|     </div> | ||||
| {% else %} | ||||
|     <div class="small dimmed">{{ "LOADING" | translate | safe }}</div> | ||||
| {% endif %} | ||||
| @@ -1,34 +1,36 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Node Helper: Newsfeed - NewsfeedFetcher | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const Log = require("../../../js/logger.js"); | ||||
|  | ||||
| const stream = require("stream"); | ||||
| const FeedMe = require("feedme"); | ||||
| const request = require("request"); | ||||
| const iconv = require("iconv-lite"); | ||||
| const { htmlToText } = require("html-to-text"); | ||||
| const Log = require("logger"); | ||||
| const NodeHelper = require("node_helper"); | ||||
|  | ||||
| /** | ||||
|  * Responsible for requesting an update on the set interval and broadcasting the data. | ||||
|  * | ||||
|  * @param {string} url URL of the news feed. | ||||
|  * @param {number} reloadInterval Reload interval in milliseconds. | ||||
|  * @param {string} encoding Encoding of the feed. | ||||
|  * @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article. | ||||
|  * @param {boolean} useCorsProxy If true cors proxy is used for article url's. | ||||
|  * @class | ||||
|  */ | ||||
| const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) { | ||||
| 	const self = this; | ||||
|  | ||||
| const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings, useCorsProxy) { | ||||
| 	let reloadTimer = null; | ||||
| 	let items = []; | ||||
| 	let reloadIntervalMS = reloadInterval; | ||||
|  | ||||
| 	let fetchFailedCallback = function () {}; | ||||
| 	let itemsReceivedCallback = function () {}; | ||||
|  | ||||
| 	if (reloadInterval < 1000) { | ||||
| 		reloadInterval = 1000; | ||||
| 	if (reloadIntervalMS < 1000) { | ||||
| 		reloadIntervalMS = 1000; | ||||
| 	} | ||||
|  | ||||
| 	/* private methods */ | ||||
| @@ -36,14 +38,14 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings | ||||
| 	/** | ||||
| 	 * Request the new items. | ||||
| 	 */ | ||||
| 	const fetchNews = function () { | ||||
| 	const fetchNews = () => { | ||||
| 		clearTimeout(reloadTimer); | ||||
| 		reloadTimer = null; | ||||
| 		items = []; | ||||
|  | ||||
| 		const parser = new FeedMe(); | ||||
|  | ||||
| 		parser.on("item", function (item) { | ||||
| 		parser.on("item", (item) => { | ||||
| 			const title = item.title; | ||||
| 			let description = item.description || item.summary || item.content || ""; | ||||
| 			const pubdate = item.pubdate || item.published || item.updated || item["dc:date"]; | ||||
| @@ -52,49 +54,74 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings | ||||
| 			if (title && pubdate) { | ||||
| 				const regex = /(<([^>]+)>)/gi; | ||||
| 				description = description.toString().replace(regex, ""); | ||||
| 				// Convert HTML entities, codes and tag | ||||
| 				description = htmlToText(description, { wordwrap: false }); | ||||
|  | ||||
| 				items.push({ | ||||
| 					title: title, | ||||
| 					description: description, | ||||
| 					pubdate: pubdate, | ||||
| 					url: url | ||||
| 					url: url, | ||||
| 					useCorsProxy: useCorsProxy | ||||
| 				}); | ||||
| 			} else if (logFeedWarnings) { | ||||
| 				Log.warn("Can't parse feed item:"); | ||||
| 				Log.warn(item); | ||||
| 				Log.warn("Title: " + title); | ||||
| 				Log.warn("Description: " + description); | ||||
| 				Log.warn("Pubdate: " + pubdate); | ||||
| 				Log.warn(`Title: ${title}`); | ||||
| 				Log.warn(`Description: ${description}`); | ||||
| 				Log.warn(`Pubdate: ${pubdate}`); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		parser.on("end", function () { | ||||
| 			self.broadcastItems(); | ||||
| 		parser.on("end", () => { | ||||
| 			this.broadcastItems(); | ||||
| 		}); | ||||
|  | ||||
| 		parser.on("error", (error) => { | ||||
| 			fetchFailedCallback(this, error); | ||||
| 			scheduleTimer(); | ||||
| 		}); | ||||
|  | ||||
| 		parser.on("error", function (error) { | ||||
| 			fetchFailedCallback(self, error); | ||||
| 		//"end" event is not broadcast if the feed is empty but "finish" is used for both | ||||
| 		parser.on("finish", () => { | ||||
| 			scheduleTimer(); | ||||
| 		}); | ||||
|  | ||||
| 		parser.on("ttl", (minutes) => { | ||||
| 			try { | ||||
| 				// 86400000 = 24 hours is mentioned in the docs as maximum value: | ||||
| 				const ttlms = Math.min(minutes * 60 * 1000, 86400000); | ||||
| 				if (ttlms > reloadIntervalMS) { | ||||
| 					reloadIntervalMS = ttlms; | ||||
| 					Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadIntervalMS} for url ${url}`); | ||||
| 				} | ||||
| 			} catch (error) { | ||||
| 				Log.warn(`Newsfeed-Fetcher: feed ttl is no valid integer=${minutes} for url ${url}`); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); | ||||
| 		const opts = { | ||||
| 			headers: { | ||||
| 				"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)", | ||||
| 				"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", | ||||
| 				Pragma: "no-cache" | ||||
| 			}, | ||||
| 			encoding: null | ||||
| 		const headers = { | ||||
| 			"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`, | ||||
| 			"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", | ||||
| 			Pragma: "no-cache" | ||||
| 		}; | ||||
|  | ||||
| 		request(url, opts) | ||||
| 			.on("error", function (error) { | ||||
| 				fetchFailedCallback(self, error); | ||||
| 				scheduleTimer(); | ||||
| 		fetch(url, { headers: headers }) | ||||
| 			.then(NodeHelper.checkFetchStatus) | ||||
| 			.then((response) => { | ||||
| 				let nodeStream; | ||||
| 				if (response.body instanceof stream.Readable) { | ||||
| 					nodeStream = response.body; | ||||
| 				} else { | ||||
| 					nodeStream = stream.Readable.fromWeb(response.body); | ||||
| 				} | ||||
| 				nodeStream.pipe(iconv.decodeStream(encoding)).pipe(parser); | ||||
| 			}) | ||||
| 			.pipe(iconv.decodeStream(encoding)) | ||||
| 			.pipe(parser); | ||||
| 			.catch((error) => { | ||||
| 				fetchFailedCallback(this, error); | ||||
| 				scheduleTimer(); | ||||
| 			}); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| @@ -104,19 +131,18 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings | ||||
| 		clearTimeout(reloadTimer); | ||||
| 		reloadTimer = setTimeout(function () { | ||||
| 			fetchNews(); | ||||
| 		}, reloadInterval); | ||||
| 		}, reloadIntervalMS); | ||||
| 	}; | ||||
|  | ||||
| 	/* public methods */ | ||||
|  | ||||
| 	/** | ||||
| 	 * Update the reload interval, but only if we need to increase the speed. | ||||
| 	 * | ||||
| 	 * @param {number} interval Interval for the update in milliseconds. | ||||
| 	 */ | ||||
| 	this.setReloadInterval = function (interval) { | ||||
| 		if (interval > 1000 && interval < reloadInterval) { | ||||
| 			reloadInterval = interval; | ||||
| 		if (interval > 1000 && interval < reloadIntervalMS) { | ||||
| 			reloadIntervalMS = interval; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| @@ -135,8 +161,8 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings | ||||
| 			Log.info("Newsfeed-Fetcher: No items to broadcast yet."); | ||||
| 			return; | ||||
| 		} | ||||
| 		Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items."); | ||||
| 		itemsReceivedCallback(self); | ||||
| 		Log.info(`Newsfeed-Fetcher: Broadcasting ${items.length} items.`); | ||||
| 		itemsReceivedCallback(this); | ||||
| 	}; | ||||
|  | ||||
| 	this.onReceive = function (callback) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| /* Magic Mirror | ||||
| /* MagicMirror² | ||||
|  * Node Helper: Newsfeed | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
| @@ -6,14 +6,13 @@ | ||||
|  */ | ||||
|  | ||||
| const NodeHelper = require("node_helper"); | ||||
| const validUrl = require("valid-url"); | ||||
| const NewsfeedFetcher = require("./newsfeedfetcher.js"); | ||||
| const Log = require("../../../js/logger"); | ||||
| const Log = require("logger"); | ||||
| const NewsfeedFetcher = require("./newsfeedfetcher"); | ||||
|  | ||||
| module.exports = NodeHelper.create({ | ||||
| 	// Override start method. | ||||
| 	start: function () { | ||||
| 		Log.log("Starting node helper for: " + this.name); | ||||
| 		Log.log(`Starting node helper for: ${this.name}`); | ||||
| 		this.fetchers = []; | ||||
| 	}, | ||||
|  | ||||
| @@ -27,39 +26,44 @@ module.exports = NodeHelper.create({ | ||||
| 	/** | ||||
| 	 * Creates a fetcher for a new feed if it doesn't exist yet. | ||||
| 	 * Otherwise it reuses the existing one. | ||||
| 	 * | ||||
| 	 * @param {object} feed The feed object. | ||||
| 	 * @param {object} config The configuration object. | ||||
| 	 * @param {object} feed The feed object | ||||
| 	 * @param {object} config The configuration object | ||||
| 	 */ | ||||
| 	createFetcher: function (feed, config) { | ||||
| 		const url = feed.url || ""; | ||||
| 		const encoding = feed.encoding || "UTF-8"; | ||||
| 		const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000; | ||||
| 		let useCorsProxy = feed.useCorsProxy; | ||||
| 		if (useCorsProxy === undefined) useCorsProxy = true; | ||||
|  | ||||
| 		if (!validUrl.isUri(url)) { | ||||
| 			this.sendSocketNotification("INCORRECT_URL", url); | ||||
| 		try { | ||||
| 			new URL(url); | ||||
| 		} catch (error) { | ||||
| 			Log.error("Newsfeed Error. Malformed newsfeed url: ", url, error); | ||||
| 			this.sendSocketNotification("NEWSFEED_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" }); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		let fetcher; | ||||
| 		if (typeof this.fetchers[url] === "undefined") { | ||||
| 			Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval); | ||||
| 			fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings); | ||||
| 			Log.log(`Create new newsfetcher for url: ${url} - Interval: ${reloadInterval}`); | ||||
| 			fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings, useCorsProxy); | ||||
|  | ||||
| 			fetcher.onReceive(() => { | ||||
| 				this.broadcastFeeds(); | ||||
| 			}); | ||||
|  | ||||
| 			fetcher.onError((fetcher, error) => { | ||||
| 				this.sendSocketNotification("FETCH_ERROR", { | ||||
| 					url: fetcher.url(), | ||||
| 					error: error | ||||
| 				Log.error("Newsfeed Error. Could not fetch newsfeed: ", url, error); | ||||
| 				let error_type = NodeHelper.checkFetchError(error); | ||||
| 				this.sendSocketNotification("NEWSFEED_ERROR", { | ||||
| 					error_type | ||||
| 				}); | ||||
| 			}); | ||||
|  | ||||
| 			this.fetchers[url] = fetcher; | ||||
| 		} else { | ||||
| 			Log.log("Use existing news fetcher for url: " + url); | ||||
| 			Log.log(`Use existing newsfetcher for url: ${url}`); | ||||
| 			fetcher = this.fetchers[url]; | ||||
| 			fetcher.setReloadInterval(reloadInterval); | ||||
| 			fetcher.broadcastItems(); | ||||
| @@ -73,8 +77,8 @@ module.exports = NodeHelper.create({ | ||||
| 	 * and broadcasts these using sendSocketNotification. | ||||
| 	 */ | ||||
| 	broadcastFeeds: function () { | ||||
| 		var feeds = {}; | ||||
| 		for (var f in this.fetchers) { | ||||
| 		const feeds = {}; | ||||
| 		for (let f in this.fetchers) { | ||||
| 			feeds[f] = this.fetchers[f].items(); | ||||
| 		} | ||||
| 		this.sendSocketNotification("NEWS_ITEMS", feeds); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user