mirror of
				https://github.com/MichMich/MagicMirror.git
				synced 2025-10-31 10:48:10 +00:00 
			
		
		
		
	Compare commits
	
		
			789 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 62b0f7f26e | ||
|  | 8e0b8468d3 | ||
|  | 39a614e0de | ||
|  | 9c9a5359dd | ||
|  | c24de64d77 | ||
|  | 94c3c699e8 | ||
|  | 53fc814ff8 | ||
|  | 5ea8a3469a | ||
|  | 118e21238c | ||
|  | 8c0e7db494 | ||
|  | d20d9a7ef8 | ||
|  | b8e0e2a791 | ||
|  | a927eb20d9 | ||
|  | aad3eefc62 | ||
|  | ee1960ced0 | ||
|  | 0b70274a1a | ||
|  | 4e7b68a69d | ||
|  | 786ea86e0e | ||
|  | d397568062 | ||
|  | a7af76b619 | ||
|  | 319a921f75 | ||
|  | d5406f4900 | ||
|  | 55cd03576f | ||
|  | 9d97724401 | ||
|  | 74854387cd | ||
|  | e77f10b86d | ||
|  | 6ffdc7b55b | ||
|  | 7098f1e41f | ||
|  | 679a413788 | ||
|  | 247115d2e4 | ||
|  | 70ddd80632 | ||
|  | 203e8647d4 | ||
|  | 3fe5ad4b3d | ||
|  | b300191609 | ||
|  | 296df06c21 | ||
|  | fe882bf92a | ||
|  | 2a6e2aacdc | ||
|  | 3a01acd389 | ||
|  | a8d06ae74e | ||
|  | 04f0df269a | ||
|  | f80889d953 | ||
|  | 6815dfa02b | ||
|  | bbc27f5ae2 | ||
|  | f46b226940 | ||
|  | 764ca3ac5c | ||
|  | 0e2da630d5 | ||
|  | a0b444d6c4 | ||
|  | 5d2ddbd3dd | ||
|  | b067711ede | ||
|  | 66b29ec26e | ||
|  | 343e7de7bd | ||
|  | 6ea94e4512 | ||
|  | 290b350856 | ||
|  | 9566d6c9a0 | ||
|  | 6b204cda25 | ||
|  | e530c783f8 | ||
|  | a3c2e7b816 | ||
|  | ad665a7a33 | ||
|  | 95ec3096e0 | ||
|  | a67a0b677c | ||
|  | 8b1c279c07 | ||
|  | 4eccce3f77 | ||
|  | af0fe37f70 | ||
|  | e5adbea49c | ||
|  | 7127979c6f | ||
|  | fa7c7fc8cf | ||
|  | 91fd931a58 | ||
|  | 7a1591b2d6 | ||
|  | f2957f90df | ||
|  | ffdf321e23 | ||
|  | 79e99e18ea | ||
|  | a92b3d3f71 | ||
|  | 5cbdd28db3 | ||
|  | 9d49196e69 | ||
|  | ef20fe2d11 | ||
|  | c0a5f35a00 | ||
|  | 2ad463b6c7 | ||
|  | 200db181d5 | ||
|  | 7ba96aeb98 | ||
|  | 7c64d8fce6 | ||
|  | 7dcea98e99 | ||
|  | 59e9d765e2 | ||
|  | 156db32c76 | ||
|  | 58cdfa3cb1 | ||
|  | 49c72d8dfc | ||
|  | 1bd146f52e | ||
|  | 948910d304 | ||
|  | 0b97639341 | ||
|  | f802c85a38 | ||
|  | 62eb23ba6a | ||
|  | 4b0e0aa48f | ||
|  | e9f1bd9d7a | ||
|  | e87f50e64a | ||
|  | 46bca1bc6d | ||
|  | 2b6720e6e5 | ||
|  | ea818bf899 | ||
|  | 0e00e64493 | ||
|  | 3c35d346ee | ||
|  | 675e4d4f67 | ||
|  | c1850f2577 | ||
|  | e985e99036 | ||
|  | b7371538bc | ||
|  | a56b92990d | ||
|  | c7405b76b3 | ||
|  | eceec8285d | ||
|  | 0573d6e772 | ||
|  | babd22b04f | ||
|  | 432d900ecd | ||
|  | 83315f1fed | ||
|  | e09d60d1d1 | ||
|  | d832d795df | ||
|  | a41aa48dd1 | ||
|  | b80485b52f | ||
|  | 7e58b38ddf | ||
|  | 979f4ec664 | ||
|  | 4e3369062e | ||
|  | 77f9c86774 | ||
|  | dee3cd3da7 | ||
|  | 09f117c3d9 | ||
|  | 32192d1698 | ||
|  | 2c7beeaaaf | ||
|  | 0d3ad9812c | ||
|  | b7eb21e48f | ||
|  | 9703226c73 | ||
|  | cc11b77f24 | ||
|  | abe5c08a52 | ||
|  | c5a8b85f4e | ||
|  | fa40a3e8e8 | ||
|  | 6223584392 | ||
|  | b5a22bc09b | ||
|  | 4ef030af5f | ||
|  | 5f38c53260 | ||
|  | d5395ee3f8 | ||
|  | ab0876f07a | ||
|  | d276a7ddb9 | ||
|  | 8f8945d418 | ||
|  | 6d779235cf | ||
|  | beea754514 | ||
|  | c6db22524a | ||
|  | 23ee155ded | ||
|  | 1b2785cc56 | ||
|  | b5b61246e6 | ||
|  | 498b440174 | ||
|  | fe0b915a5d | ||
|  | 2b792cdbb8 | ||
|  | a23769156e | ||
|  | 6d86ffade4 | ||
|  | 390e5d6490 | ||
|  | b08a4737af | ||
|  | bf28e63709 | ||
|  | fb22a76796 | ||
|  | 81244d961e | ||
|  | 65aa1b0ddc | ||
|  | 88c7e42368 | ||
|  | e24dfa6b1a | ||
|  | a65ee86501 | ||
|  | 4b478a5a5e | ||
|  | f14e956166 | ||
|  | 1dc0a0d5b5 | ||
|  | bf279d9a57 | ||
|  | 42d42ef452 | ||
|  | ed90f0546f | ||
|  | a8dc563a31 | ||
|  | 58b9ddcd9f | ||
|  | 7198ae5eae | ||
|  | f6dcfb5ca3 | ||
|  | 157e74ce7c | ||
|  | 67e4dbaacd | ||
|  | 2e2962d492 | ||
|  | cd4ba428da | ||
|  | ee8695637b | ||
|  | 4244c05764 | ||
|  | c714399b4d | ||
|  | 8d9f132666 | ||
|  | d2327d3d6f | ||
|  | 29e3ec06cb | ||
|  | 877f8ad380 | ||
|  | 6e80e5a295 | ||
|  | 7bc91a742f | ||
|  | 2eaf9dfeeb | ||
|  | fc303146a5 | ||
|  | 2908c15ea6 | ||
|  | a975b44fbb | ||
|  | 4fc38bd5bb | ||
|  | c99f660d98 | ||
|  | 0300ce05d5 | ||
|  | cd739b6912 | ||
|  | 0ebedd0fb8 | ||
|  | e9be668d1b | ||
|  | 76d9042e60 | ||
|  | 2fec314ff5 | ||
|  | 3124b0a9c5 | ||
|  | a2624442cc | ||
|  | eee289aee8 | ||
|  | abbae90a8f | ||
|  | bd0b3c00ad | ||
|  | b9b7d2c95d | ||
|  | 0b01e9dbe0 | ||
|  | 4fecffc3df | ||
|  | 4d47c0837f | ||
|  | 3879949f58 | ||
|  | f25abfd2f8 | ||
|  | 7058fc5fd8 | ||
|  | 00bc6eb28c | ||
|  | f79d3f007d | ||
|  | c191ff0032 | ||
|  | dde88601a6 | ||
|  | 2d3940a4ff | ||
|  | 64ed5a54cb | ||
|  | 7bd944391e | ||
|  | ad4dbd786a | ||
|  | fc59ed20e3 | ||
|  | 835c893205 | ||
|  | 7bbf8c19db | ||
|  | 1eb2965b2b | ||
|  | 85a9f14178 | ||
|  | a328ce537f | ||
|  | 21ae79b386 | ||
|  | d5e855dd6d | ||
|  | a86e27a12c | ||
|  | f434be3d44 | ||
|  | ce4906d13b | ||
|  | 8212d30c4c | ||
|  | f04d578704 | ||
|  | 7694d6fa86 | ||
|  | 9e0293047f | ||
|  | 298e585361 | ||
|  | 21a6d1bcea | ||
|  | bbe17b9b01 | ||
|  | 4381fc6695 | ||
|  | 818fd7b490 | ||
|  | 4c6c6f9ed3 | ||
|  | 2338a90191 | ||
|  | 6cad0e191b | ||
|  | f23e604ed4 | ||
|  | 0c1abad9df | ||
|  | fb96cc3c72 | ||
|  | e917f40542 | ||
|  | 29d467715f | ||
|  | b791a3761f | ||
|  | 02201d9f15 | ||
|  | b8dbf95497 | ||
|  | 65022f3ce1 | ||
|  | 44854d6a4f | ||
|  | 203014c654 | ||
|  | d3e53586fd | ||
|  | 9dd343054e | ||
|  | 11d17dd2c0 | ||
|  | 4cc3e481cc | ||
|  | 174da38cc8 | ||
|  | 0a35505e8d | ||
|  | 032f7ac299 | ||
|  | ca906c4b36 | ||
|  | 50f72f09ac | ||
|  | 02cf9b37e2 | ||
|  | 6f273d76b3 | ||
|  | 0a1067ec7d | ||
|  | 48756e8774 | ||
|  | 4915ad8fc7 | ||
|  | ba128cbae9 | ||
|  | 2a6e51493e | ||
|  | ef678f9f8a | ||
|  | e997ee7071 | ||
|  | 1273390af3 | ||
|  | 0f6eb4a244 | ||
|  | 2c0c62c89b | ||
|  | ec13c952d9 | ||
|  | 6fc71cf6f8 | ||
|  | bd3137a3dc | ||
|  | b26da4f97f | ||
|  | cde0adc28e | ||
|  | 2813f101b8 | ||
|  | 4d8fb8c176 | ||
|  | 9ae62d60f7 | ||
|  | 17c14e137e | ||
|  | bc239f6608 | ||
|  | 6493fad8a4 | ||
|  | d539f459ca | ||
|  | 2cca6a2f39 | ||
|  | 80e07bae0d | ||
|  | d4c4f6e1a5 | ||
|  | d24fe4e983 | ||
|  | aaa9042810 | ||
|  | a4bb1cefb9 | ||
|  | c3339b47bb | ||
|  | 0c1e5ea881 | ||
|  | 3fbd9006ad | ||
|  | be9761146c | ||
|  | 5aa9e7e0f6 | ||
|  | 65e87aea52 | ||
|  | 66fffc932c | ||
|  | 1e934e16af | ||
|  | 82fbb7e32d | ||
|  | 8bf9b9bef9 | ||
|  | 2d15e4f976 | ||
|  | 055ce56a57 | ||
|  | f1f2a61dc8 | ||
|  | 39c1b37726 | ||
|  | 5b1b25fa86 | ||
|  | 54ab0b1bf0 | ||
|  | 5507e9ffe9 | ||
|  | 30d5bfe59e | ||
|  | b716ec33d9 | ||
|  | e25209d9f9 | ||
|  | 1f4ac82495 | ||
|  | 0baf58f3fd | ||
|  | 9a769203e3 | ||
|  | 1435efaea7 | ||
|  | c411ac821e | ||
|  | da0489fc0c | ||
|  | c82de1e314 | ||
|  | 604a555e14 | ||
|  | 87e011ff96 | ||
|  | 9b2051827c | ||
|  | 47aefb0c82 | ||
|  | 70da1d6f5c | ||
|  | c0743ce9de | ||
|  | fe7b4044e0 | ||
|  | 701ce8ad47 | ||
|  | 591f907134 | ||
|  | 4f1db749c0 | ||
|  | ae72ed8c67 | ||
|  | ab7934fa98 | ||
|  | a4c77f0c9e | ||
|  | 0023c64d59 | ||
|  | b55b3bd63b | ||
|  | cbda20f67e | ||
|  | e598dbb206 | ||
|  | 094881fc5c | ||
|  | b13414cab8 | ||
|  | 3e65ac653b | ||
|  | 05621c9876 | ||
|  | 938619ce0c | ||
|  | 8e5267d44f | ||
|  | 3b55886c45 | ||
|  | ed3aceb427 | ||
|  | c3b2aaec69 | ||
|  | bb1d3431cc | ||
|  | fe83fe338a | ||
|  | 6504b5e818 | ||
|  | 8578900bfb | ||
|  | d730dd04bf | ||
|  | 1d90c5e1fe | ||
|  | 0f39b7733c | ||
|  | 038b6765e7 | ||
|  | 58569a648c | ||
|  | df0f048ecc | ||
|  | 288a008e72 | ||
|  | 439690b981 | ||
|  | b4e75f6844 | ||
|  | 3f6a5a7772 | ||
|  | a2d7cdcfb4 | ||
|  | d995ce38ea | ||
|  | 0b21a22027 | ||
|  | 111d75b351 | ||
|  | b54cac72ec | ||
|  | 69b2dd698c | ||
|  | 16bf19d115 | ||
|  | c57c9c2a8f | ||
|  | 9f750e5980 | ||
|  | 8d296e563d | ||
|  | 88f7570caf | ||
|  | 00a7c6b5be | ||
|  | 612b06346d | ||
|  | 9620566fbb | ||
|  | ed65c70a36 | ||
|  | cdd87436be | ||
|  | a143ebc8e0 | ||
|  | 93581ca4d9 | ||
|  | c5e9c9a683 | ||
|  | 1bdab10872 | ||
|  | eca339ad60 | ||
|  | af5eb3447f | ||
|  | b72cb52a71 | ||
|  | d0565a15d3 | ||
|  | 468bf1d6a0 | ||
|  | 1d2637c980 | ||
|  | a52c68d852 | ||
|  | e12f57d872 | ||
|  | 5def02ff7f | ||
|  | 7cab6eb680 | ||
|  | dcaa3526a4 | ||
|  | 2a989bc235 | ||
|  | d588fab981 | ||
|  | 9cb006b871 | ||
|  | 9056abaf4a | ||
|  | 3c27fd10b6 | ||
|  | 4048d79fc5 | ||
|  | 212e60c12d | ||
|  | 45142bc1bc | ||
|  | e791a663c8 | ||
|  | 81ae95eba7 | ||
|  | 7ca5d81123 | ||
|  | e234d2379b | ||
|  | d0838d53c2 | ||
|  | 880e2160a3 | ||
|  | c214d776df | ||
|  | 0a8220ca52 | ||
|  | b0c57272c2 | ||
|  | fbdebcae8a | ||
|  | 8e376deaaa | ||
|  | 2d353ffa35 | ||
|  | b70a9c36fa | ||
|  | c98967cb1e | ||
|  | 05f0d1855c | ||
|  | cee5043625 | ||
|  | 528358f3c3 | ||
|  | da90412cea | ||
|  | e794aab012 | ||
|  | 69daf14ffd | ||
|  | afa6c4270d | ||
|  | be22309e45 | ||
|  | 6f27e5ae07 | ||
|  | 7e79547973 | ||
|  | a5668b1b99 | ||
|  | f04dd6b6cd | ||
|  | 9f9c17ea8a | ||
|  | 48845ab60e | ||
|  | 59bc2318f8 | ||
|  | c622db918b | ||
|  | 4b381f33b5 | ||
|  | 7cfc7b9d74 | ||
|  | 7f52f9bcf2 | ||
|  | c4e7e42cdd | ||
|  | d181365620 | ||
|  | 97b474665a | ||
|  | 284b01c524 | ||
|  | e3857ca0f4 | ||
|  | 0aee42255f | ||
|  | fcb0d33e29 | ||
|  | 3a761f2082 | ||
|  | ed0ad7b988 | ||
|  | c392b5a661 | ||
|  | 766848eea3 | ||
|  | 7ede537d1b | ||
|  | ec38f90662 | ||
|  | d793b82d51 | ||
|  | aafeb1e875 | ||
|  | 480c10b239 | ||
|  | 82bb2544de | ||
|  | b714abd9a0 | ||
|  | c1be0180f9 | ||
|  | 2cfafe7bfe | ||
|  | da8edbfdc1 | ||
|  | 54bcbbc1de | ||
|  | 5463183e01 | ||
|  | 42b80b18f8 | ||
|  | 65fada7ef1 | ||
|  | 9e4997aa81 | ||
|  | e8c6ef945a | ||
|  | 14a22efae1 | ||
|  | f7f0bc8681 | ||
|  | 9cb8a135e6 | ||
|  | a02bce6517 | ||
|  | 9604c3a187 | ||
|  | 1ba4213910 | ||
|  | 6c63fac240 | ||
|  | b3dd531abb | ||
|  | 4ce42d4f70 | ||
|  | 4325fcdca2 | ||
|  | 8cf9d5f3ed | ||
|  | 8cb6015930 | ||
|  | f3cefde7cb | ||
|  | 92fcdde60d | ||
|  | 2dcf55ea89 | ||
|  | 8537f40f1d | ||
|  | 8417590893 | ||
|  | 4c43f5db15 | ||
|  | eb32ec89b4 | ||
|  | ad66c02735 | ||
|  | e8c0611db4 | ||
|  | 7fa54642e1 | ||
|  | 0e6d40f96c | ||
|  | cd1fe4e182 | ||
|  | c56b601ab8 | ||
|  | e78fa11fd4 | ||
|  | 1619dd29e9 | ||
|  | 793f3dd75c | ||
|  | ce05f8e958 | ||
|  | c8c4585946 | ||
|  | 990d1adfb2 | ||
|  | 404343de1d | ||
|  | 24bfaaca7e | ||
|  | 060ca43fc8 | ||
|  | b87e802c8d | ||
|  | 0f596d5620 | ||
|  | 3ebdb67bc0 | ||
|  | a5668ef729 | ||
|  | 24fccf6c44 | ||
|  | e12692252e | ||
|  | a6cbc9f0ef | ||
|  | be073daaf2 | ||
|  | 4c919a7489 | ||
|  | 7a7ed48063 | ||
|  | 01010fe795 | ||
|  | 32382dc461 | ||
|  | bf83341fb9 | ||
|  | c4d2a7b409 | ||
|  | 4ad832dcdd | ||
|  | b35d25eda4 | ||
|  | da51a5512f | ||
|  | 710d51bf32 | ||
|  | 446bb229bc | ||
|  | c605023bad | ||
|  | 28d866c001 | ||
|  | 95b587381b | ||
|  | fc14431147 | ||
|  | 5d8f2f5da1 | ||
|  | cf428dc1f7 | ||
|  | c579989ded | ||
|  | a954a62762 | ||
|  | 855860c00c | ||
|  | 58c48b1b21 | ||
|  | 512c392386 | ||
|  | c5ed70dbfc | ||
|  | f75ca0c399 | ||
|  | 75c2977daf | ||
|  | b3ef4b40c5 | ||
|  | bd5664cf8e | ||
|  | 272219eb61 | ||
|  | acbcb47739 | ||
|  | 672575e427 | ||
|  | be0b3b1d63 | ||
|  | c88d25b4ee | ||
|  | 9b83862a96 | ||
|  | 21c7f0da6e | ||
|  | dca5759569 | ||
|  | 0abf87bfa2 | ||
|  | 6b8b37843b | ||
|  | fa0b83f056 | ||
|  | a706fa3590 | ||
|  | 852aa3d260 | ||
|  | c4edcaad87 | ||
|  | 9d0cab01d5 | ||
|  | c3e507234d | ||
|  | 405ec82dd0 | ||
|  | 9c4c77fe84 | ||
|  | 831afdf9e7 | ||
|  | 1996efb183 | ||
|  | 19bb2a0238 | ||
|  | af6cf70558 | ||
|  | 9b57e6049e | ||
|  | f54c06fb94 | ||
|  | 2107e7c427 | ||
|  | eae277f165 | ||
|  | a9c4ad2895 | ||
|  | e88ba1ab1c | ||
|  | 677dcb87ef | ||
|  | 7bb217636e | ||
|  | abc16e98eb | ||
|  | 3b1405609e | ||
|  | b0d3518c1d | ||
|  | aed005618e | ||
|  | 559970c95d | ||
|  | f6f3aa11ea | ||
|  | 472c91f022 | ||
|  | 4a7f498683 | ||
|  | 2b87d6ec01 | ||
|  | 1af282a7a1 | ||
|  | 46b63f52fe | ||
|  | 01977005fb | ||
|  | b1a46b365b | ||
|  | 3a0a02d3ba | ||
|  | 1acbef5bfa | ||
|  | 66b2ba07bb | ||
|  | 4777538103 | ||
|  | 31d31fc3d3 | ||
|  | 21f606c6ba | ||
|  | 6508ec2d17 | ||
|  | c623ca9fe0 | ||
|  | 90deaf564f | ||
|  | e2d2cf67fa | ||
|  | 8ee73a11bb | ||
|  | b5b01be373 | ||
|  | 730f2eb0b8 | ||
|  | 4576754de2 | ||
|  | 0fb9e0bc89 | ||
|  | 000c34ef73 | ||
|  | 0ec80a7791 | ||
|  | e9650285bd | ||
|  | d0cc0a4034 | ||
|  | f9c4a3a9c0 | ||
|  | 15fd2021bb | ||
|  | 75cf1d610e | ||
|  | 8f5ee9466a | ||
|  | 467720f1c4 | ||
|  | 026e624e23 | ||
|  | 460a9fc5f7 | ||
|  | 3695e64ce9 | ||
|  | b3bb829e4d | ||
|  | 1b9f8c23d2 | ||
|  | 5ee5fd7332 | ||
|  | 18d94b9a26 | ||
|  | 3ae9686b2b | ||
|  | bbe4ef8497 | ||
|  | a2fd354dc9 | ||
|  | a7202078ce | ||
|  | d8940a9cea | ||
|  | 96611333bf | ||
|  | 5074123f57 | ||
|  | 5bfbd3eaa0 | ||
|  | 7fdeed14f5 | ||
|  | 04a9ca92b5 | ||
|  | 016a1e9adb | ||
|  | 1d12e57606 | ||
|  | cb753f5371 | ||
|  | 7a9f9b2705 | ||
|  | 6d4b4dd9fc | ||
|  | 3d9c92fb63 | ||
|  | d4168f6b5d | ||
|  | 1751cabb9d | ||
|  | b0e3b6414a | ||
|  | 51967ed9f5 | ||
|  | fc06f13c30 | ||
|  | b094707324 | ||
|  | e4a0a517d5 | ||
|  | 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 | 
| @@ -1 +0,0 @@ | ||||
| modules/default/calendar/vendor/* | ||||
| @@ -1,30 +0,0 @@ | ||||
| { | ||||
| 	"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"], | ||||
| 	"plugins": ["prettier", "jsdoc", "jest"], | ||||
| 	"env": { | ||||
| 		"browser": true, | ||||
| 		"es6": true, | ||||
| 		"jest/globals": true, | ||||
| 		"node": true | ||||
| 	}, | ||||
| 	"globals": { | ||||
| 		"config": true, | ||||
| 		"Log": true, | ||||
| 		"MM": true, | ||||
| 		"Module": true, | ||||
| 		"moment": true | ||||
| 	}, | ||||
| 	"parserOptions": { | ||||
| 		"sourceType": "module", | ||||
| 		"ecmaVersion": 2017, | ||||
| 		"ecmaFeatures": { | ||||
| 			"globalReturn": true | ||||
| 		} | ||||
| 	}, | ||||
| 	"rules": { | ||||
| 		"prettier/prettier": "error", | ||||
| 		"eqeqeq": "error", | ||||
| 		"no-prototype-builtins": "off", | ||||
| 		"no-unused-vars": "off" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										56
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| # .gitattributes snippet to force users to use same line endings for project. | ||||
| #  | ||||
| # Handle line endings automatically for files detected as text | ||||
| # and leave all files detected as binary untouched. | ||||
| * text=auto | ||||
|  | ||||
| # | ||||
| # The above will handle all files NOT found below | ||||
| # https://help.github.com/articles/dealing-with-line-endings/ | ||||
| # https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes | ||||
|  | ||||
|  | ||||
|  | ||||
| # These files are text and should be normalized (Convert crlf => lf) | ||||
| *.php text | ||||
| *.css text | ||||
| *.scss text | ||||
| *.js text | ||||
| *.json text | ||||
| *.htm text | ||||
| *.html text | ||||
| *.xml text | ||||
| *.txt text | ||||
| *.ini text | ||||
| *.inc text | ||||
| *.pl text | ||||
| *.rb text | ||||
| *.py text | ||||
| *.scm text | ||||
| *.sql text | ||||
| .htaccess text | ||||
| *.sh text | ||||
| Dockerfile* text | ||||
| *.yml text | ||||
| *.yaml text | ||||
| *.md text | ||||
| *.markdown text | ||||
|  | ||||
| # These files are binary and should be left untouched | ||||
| # (binary is a macro for -text -diff) | ||||
| *.png binary | ||||
| *.jpg binary | ||||
| *.jpeg binary | ||||
| *.gif binary | ||||
| *.ico binary | ||||
| *.mov binary | ||||
| *.mp4 binary | ||||
| *.mp3 binary | ||||
| *.flv binary | ||||
| *.fla binary | ||||
| *.swf binary | ||||
| *.gz binary | ||||
| *.zip binary | ||||
| *.7z binary | ||||
| *.ttf binary | ||||
| *.pyc binary | ||||
							
								
								
									
										137
									
								
								.github/CODE_OF_CONDUCT.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								.github/CODE_OF_CONDUCT.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| # Contributor Covenant Code of Conduct | ||||
|  | ||||
| ## Our Pledge | ||||
|  | ||||
| We as members, contributors, and leaders pledge to make participation in our | ||||
| community a harassment-free experience for everyone, regardless of age, body | ||||
| size, visible or invisible disability, ethnicity, sex characteristics, gender | ||||
| identity and expression, level of experience, education, socio-economic status, | ||||
| nationality, personal appearance, race, caste, color, religion, or sexual | ||||
| identity and orientation. | ||||
|  | ||||
| We pledge to act and interact in ways that contribute to an open, welcoming, | ||||
| diverse, inclusive, and healthy community. | ||||
|  | ||||
| ## Our Standards | ||||
|  | ||||
| Examples of behavior that contributes to a positive environment for our | ||||
| community include: | ||||
|  | ||||
| - Demonstrating empathy and kindness toward other people | ||||
| - Being respectful of differing opinions, viewpoints, and experiences | ||||
| - Giving and gracefully accepting constructive feedback | ||||
| - Accepting responsibility and apologizing to those affected by our mistakes, | ||||
|   and learning from the experience | ||||
| - Focusing on what is best not just for us as individuals, but for the overall | ||||
|   community | ||||
|  | ||||
| Examples of unacceptable behavior include: | ||||
|  | ||||
| - The use of sexualized language or imagery, and sexual attention or advances of | ||||
|   any kind | ||||
| - Trolling, insulting or derogatory comments, and personal or political attacks | ||||
| - Public or private harassment | ||||
| - Publishing others' private information, such as a physical or email address, | ||||
|   without their explicit permission | ||||
| - Other conduct which could reasonably be considered inappropriate in a | ||||
|   professional setting | ||||
|  | ||||
| ## Enforcement Responsibilities | ||||
|  | ||||
| Community leaders are responsible for clarifying and enforcing our standards of | ||||
| acceptable behavior and will take appropriate and fair corrective action in | ||||
| response to any behavior that they deem inappropriate, threatening, offensive, | ||||
| or harmful. | ||||
|  | ||||
| Community leaders have the right and responsibility to remove, edit, or reject | ||||
| comments, commits, code, wiki edits, issues, and other contributions that are | ||||
| not aligned to this Code of Conduct, and will communicate reasons for moderation | ||||
| decisions when appropriate. | ||||
|  | ||||
| ## Scope | ||||
|  | ||||
| This Code of Conduct applies within all community spaces, and also applies when | ||||
| an individual is officially representing the community in public spaces. | ||||
| Examples of representing our community include using an official email address, | ||||
| posting via an official social media account, or acting as an appointed | ||||
| representative at an online or offline event. | ||||
|  | ||||
| ## Enforcement | ||||
|  | ||||
| Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||
| reported to the community leaders responsible for enforcement: | ||||
| Contact [Rejas](https://forum.magicmirror.builders/user/rejas), | ||||
| [Karsten](https://forum.magicmirror.builders/user/karsten13), | ||||
| [Sam](https://forum.magicmirror.builders/user/sdetweil) or | ||||
| [Kristjan](https://forum.magicmirror.builders/user/kristjanesperanto) | ||||
| via private message in the forum. | ||||
|  | ||||
| All complaints will be reviewed and investigated promptly and fairly. | ||||
|  | ||||
| All community leaders are obligated to respect the privacy and security of the | ||||
| reporter of any incident. | ||||
|  | ||||
| ## Enforcement Guidelines | ||||
|  | ||||
| Community leaders will follow these Community Impact Guidelines in determining | ||||
| the consequences for any action they deem in violation of this Code of Conduct: | ||||
|  | ||||
| ### 1. Correction | ||||
|  | ||||
| **Community Impact**: Use of inappropriate language or other behavior deemed | ||||
| unprofessional or unwelcome in the community. | ||||
|  | ||||
| **Consequence**: A private, written warning from community leaders, providing | ||||
| clarity around the nature of the violation and an explanation of why the | ||||
| behavior was inappropriate. A public apology may be requested. | ||||
|  | ||||
| ### 2. Warning | ||||
|  | ||||
| **Community Impact**: A violation through a single incident or series of | ||||
| actions. | ||||
|  | ||||
| **Consequence**: A warning with consequences for continued behavior. No | ||||
| interaction with the people involved, including unsolicited interaction with | ||||
| those enforcing the Code of Conduct, for a specified period of time. This | ||||
| includes avoiding interactions in community spaces as well as external channels | ||||
| like social media. Violating these terms may lead to a temporary or permanent | ||||
| ban. | ||||
|  | ||||
| ### 3. Temporary Ban | ||||
|  | ||||
| **Community Impact**: A serious violation of community standards, including | ||||
| sustained inappropriate behavior. | ||||
|  | ||||
| **Consequence**: A temporary ban from any sort of interaction or public | ||||
| communication with the community for a specified period of time. No public or | ||||
| private interaction with the people involved, including unsolicited interaction | ||||
| with those enforcing the Code of Conduct, is allowed during this period. | ||||
| Violating these terms may lead to a permanent ban. | ||||
|  | ||||
| ### 4. Permanent Ban | ||||
|  | ||||
| **Community Impact**: Demonstrating a pattern of violation of community | ||||
| standards, including sustained inappropriate behavior, harassment of an | ||||
| individual, or aggression toward or disparagement of classes of individuals. | ||||
|  | ||||
| **Consequence**: A permanent ban from any sort of public interaction within the | ||||
| community. | ||||
|  | ||||
| ## Attribution | ||||
|  | ||||
| This Code of Conduct is adapted from the [Contributor Covenant][homepage], | ||||
| version 2.1, available at | ||||
| [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. | ||||
|  | ||||
| Community Impact Guidelines were inspired by | ||||
| [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. | ||||
|  | ||||
| For answers to common questions about this code of conduct, see the FAQ at | ||||
| [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at | ||||
| [https://www.contributor-covenant.org/translations][translations]. | ||||
|  | ||||
| [homepage]: https://www.contributor-covenant.org | ||||
| [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html | ||||
| [Mozilla CoC]: https://github.com/mozilla/diversity | ||||
| [FAQ]: https://www.contributor-covenant.org/faq | ||||
| [translations]: https://www.contributor-covenant.org/translations | ||||
							
								
								
									
										45
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										45
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							| @@ -4,46 +4,35 @@ Thanks for contributing to MagicMirror²! | ||||
|  | ||||
| We hold our code to standard, and these standards are documented below. | ||||
|  | ||||
| If you wish to run our linters, use `npm run lint` without any arguments. | ||||
| ## Linters | ||||
|  | ||||
| We use [prettier](https://prettier.io/) for automatic formatting a lot all our files. The configuration is in our `prettier.config.mjs` file. | ||||
|  | ||||
| To run prettier, use `node --run lint:prettier`. | ||||
|  | ||||
| ### JavaScript: Run ESLint | ||||
|  | ||||
| We use [ESLint](https://eslint.org) on our JavaScript files. | ||||
| We use [ESLint](https://eslint.org) to lint our JavaScript files. The configuration is in our `eslint.config.mjs` file. | ||||
|  | ||||
| Our ESLint configuration is in our .eslintrc.json and .eslintignore files. | ||||
|  | ||||
| To run ESLint, use `npm run lint:js`. | ||||
| To run ESLint, use `node --run lint:js`. | ||||
|  | ||||
| ### CSS: Run StyleLint | ||||
|  | ||||
| We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file. | ||||
| We use [StyleLint](https://stylelint.io) to lint our CSS. The configuration is in our `stylelint.config.mjs` file. | ||||
|  | ||||
| To run StyleLint, use `npm run lint:style`. | ||||
| To run StyleLint, use `node --run lint:css`. | ||||
|  | ||||
| ### Submitting Issues | ||||
| ### Markdown: Run markdownlint | ||||
|  | ||||
| Please only submit reproducible issues. | ||||
| We use [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) to lint our markdown files. The configuration is in our `.markdownlint.json` file. | ||||
|  | ||||
| If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt) | ||||
| To run markdownlint, use `node --run lint:markdown`. | ||||
|  | ||||
| Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting) | ||||
| ## Testing | ||||
|  | ||||
| When submitting a new issue, please supply the following information: | ||||
| We use [Jest](https://jestjs.io) for JavaScript testing. | ||||
|  | ||||
| **Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX). | ||||
| To run all tests, use `node --run test`. | ||||
|  | ||||
| **Node Version**: Make sure it's version 10 or later. | ||||
|  | ||||
| **MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2). | ||||
|  | ||||
| **Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. | ||||
|  | ||||
| **Steps to Reproduce**: List the step by step process to reproduce the issue. | ||||
|  | ||||
| **Expected Results**: Describe what you expected to see. | ||||
|  | ||||
| **Actual Results**: Describe what you actually saw. | ||||
|  | ||||
| **Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information! | ||||
|  | ||||
| **Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional. | ||||
| The specific test commands are defined in `package.json`. | ||||
| So you can also run the specific tests with other commands, e.g. `node --run test:unit` or `npx jest tests/e2e/env_spec.js`. | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/FUNDING.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| github: MichMich | ||||
| custom: ["https://magicmirror.builders/#donate"] | ||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +0,0 @@ | ||||
| github: MichMich | ||||
| custom: ['https://magicmirror.builders/#donate'] | ||||
							
								
								
									
										48
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,48 +0,0 @@ | ||||
| Hello and thank you for opening an issue. | ||||
|  | ||||
| **Please make sure that you have read the following lines before submitting your Issue:** | ||||
|  | ||||
| ## I'm not sure if this is a bug | ||||
|  | ||||
| If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt) | ||||
|  | ||||
| ## I'm having troubles installing or configuring MagicMirror | ||||
|  | ||||
| Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting) | ||||
|  | ||||
| A common problem is that your config file could be invalid. Please run in your MagicMirror directory: `npm run config:check` and see if it reports an error. | ||||
|  | ||||
| ## I found a bug in the MagicMirror installer | ||||
|  | ||||
| If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository: | ||||
| [https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts) | ||||
|  | ||||
| ## I found a bug in the MagicMirror Docker image | ||||
|  | ||||
| If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image: | ||||
| [https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror) | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## I found a bug in MagicMirror | ||||
|  | ||||
| Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line. | ||||
| When submitting a new issue, please supply the following information: | ||||
|  | ||||
| **Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX). | ||||
|  | ||||
| **Node Version**: Make sure it's version 10 or later. | ||||
|  | ||||
| **MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file. | ||||
|  | ||||
| **Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. | ||||
|  | ||||
| **Steps to Reproduce**: List the step by step process to reproduce the issue. | ||||
|  | ||||
| **Expected Results**: Describe what you expected to see. | ||||
|  | ||||
| **Actual Results**: Describe what you actually saw. | ||||
|  | ||||
| **Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information! | ||||
|  | ||||
| **Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional. | ||||
							
								
								
									
										154
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| name: 🐛 Report a problem | ||||
| description: Report an issue with MagicMirror² 🚨 | ||||
| title: "[Bug] {{ brief description }}" | ||||
| labels: | ||||
|   - bug | ||||
| body: | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         Thanks for reporting a bug! Please fill in the following template to help us reproduce the issue. | ||||
|         Please only submit reproducible issues. If you're not sure if it's a real bug or if it's just you, please open a topic on the forum. | ||||
|   - type: textarea | ||||
|     id: environment | ||||
|     attributes: | ||||
|       label: Environment | ||||
|       description: | | ||||
|         Please tell us about how your MagicMirror² is set up. | ||||
|  | ||||
|         Optimal would be the systeminformation from the logs, which looks like this: | ||||
|         ```bash | ||||
|         [2025-01-14 20:05:03.529] [INFO]  System information: | ||||
|         ### SYSTEM:    manufacturer: Raspberry Pi Foundation; model: Raspberry Pi 4 Model B Rev 1.5; virtual: false | ||||
|         ### OS:        platform: linux; distro: Debian GNU/Linux; release: 12; arch: arm64; kernel: 6.1.21-v8+ | ||||
|         ### VERSIONS:  electron: 31.2.1; used node: 20.15.0; installed node: 22.4.1; npm: 10.8.1; pm2: | ||||
|         ### OTHER:     timeZone: Europe/Berlin; ELECTRON_ENABLE_GPU: undefined | ||||
|         ``` | ||||
|  | ||||
|         If you can't provide this information, please provide the following: | ||||
|         - MagicMirror² version: Can be found in the `package.json` file. Please use the latest version before reporting a bug. | ||||
|         - Node version: Run `node -v` to find out. Make sure it's version 20 or later (recommended is 22). | ||||
|         - npm version: Run `npm -v` to find out. | ||||
|         - Platform: Are you using a Raspberry Pi (2/3/4/5), Windows, Mac, Linux, Docker, or something else? | ||||
|       value: | | ||||
|         MagicMirror² version:  | ||||
|         Node version:  | ||||
|         npm version:  | ||||
|         Platform: | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: dropdown | ||||
|     id: start-option | ||||
|     attributes: | ||||
|       label: Which start option are you using? | ||||
|       description: | | ||||
|         Please keep in mind that some problems are specific to certain start options. | ||||
|       options: | ||||
|         - "node --run start" | ||||
|         - "node --run start:wayland" | ||||
|         - "node --run start:windows" | ||||
|         - "node --run start:x11" | ||||
|         - "node --run server" | ||||
|         - "node clientonly --address ... --port ..." | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: dropdown | ||||
|     id: pm2 | ||||
|     attributes: | ||||
|       label: Are you using PM2? | ||||
|       options: | ||||
|         - "No" | ||||
|         - "Yes" | ||||
|         - "I don't know" | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: dropdown | ||||
|     id: module | ||||
|     attributes: | ||||
|       label: Module | ||||
|       description: | | ||||
|         If the issue is related to a specific module, please provide the name of the module. | ||||
|         Note: Please don't report issues with 3rd party modules here. Report them on the module's repository. | ||||
|       options: | ||||
|         - "alert" | ||||
|         - "calendar" | ||||
|         - "clock" | ||||
|         - "compliments" | ||||
|         - "helloworld" | ||||
|         - "newsfeed" | ||||
|         - "updatenotification" | ||||
|         - "weather" | ||||
|   - type: checkboxes | ||||
|     id: module-disabled | ||||
|     attributes: | ||||
|       label: Have you tried disabling other modules? | ||||
|       options: | ||||
|         - label: "Yes" | ||||
|         - label: "No" | ||||
|   - type: checkboxes | ||||
|     id: search | ||||
|     attributes: | ||||
|       label: Have you searched if someone else has already reported the issue on the forum or in the issues? | ||||
|       options: | ||||
|         - label: "Yes" | ||||
|           required: true | ||||
|   - type: textarea | ||||
|     id: description | ||||
|     attributes: | ||||
|       label: What did you do? | ||||
|       description: | | ||||
|         Please include a *minimal* reproduction case. List the step by step process to reproduce the issue. | ||||
|         You can use Markdown in this field. | ||||
|       value: | | ||||
|         <details> | ||||
|         <summary>Configuration</summary> | ||||
|  | ||||
|         ``` | ||||
|         <!-- Paste your configuration here. Don't forget to remove any sensitive information! --> | ||||
|         ``` | ||||
|         </details> | ||||
|  | ||||
|         ```js | ||||
|         <!-- Paste relevant code here --> | ||||
|         ``` | ||||
|  | ||||
|         Steps to reproduce the issue: | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: expectation | ||||
|     attributes: | ||||
|       label: What did you expect to happen? | ||||
|       description: | | ||||
|         You can use Markdown in this field. | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: lint-output | ||||
|     attributes: | ||||
|       label: What actually happened? | ||||
|       description: | | ||||
|         Please copy-paste relevant log output or error messages. | ||||
|         You can use Markdown in this field. | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: comments | ||||
|     attributes: | ||||
|       label: Additional comments | ||||
|       description: | | ||||
|         Is there anything else that's important for the team to know? | ||||
|         Fill out all fields and provide as much information as possible. | ||||
|         Adding screenshots might help us understand your problem better. | ||||
|  | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
|       label: Participation | ||||
|       options: | ||||
|         - label: "I am willing to submit a pull request for this change." | ||||
|           required: false | ||||
|  | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: Please **do not** open a pull request until this issue has been accepted by the team. | ||||
							
								
								
									
										41
									
								
								.github/ISSUE_TEMPLATE/change_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								.github/ISSUE_TEMPLATE/change_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| name: 🔀 Request a change | ||||
| description: Request a change that is not a bug fix, a feature request or a support request. | ||||
| title: "[Change Request] {{ brief description }}" | ||||
| labels: | ||||
|   - enhancement | ||||
|   - core | ||||
| body: | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: Thanks for requesting a change! Please fill in the following template to help us understand your request. | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: What problem do you want to solve with this change? | ||||
|       description: | | ||||
|         Please explain your use case in as much detail as possible. | ||||
|       placeholder: | | ||||
|         Currently... | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: What do you think is the correct solution? | ||||
|       description: | | ||||
|         Please explain how you'd like to change MagicMirror² to address the problem. | ||||
|       placeholder: | | ||||
|         I'd like MagicMirror² to... | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
|       label: Participation | ||||
|       options: | ||||
|         - label: I am willing to submit a pull request for this change. | ||||
|           required: false | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: Please **do not** open a pull request until this issue has been accepted by the team. | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: Additional comments | ||||
|       description: Is there anything else that's important for the team to know? | ||||
							
								
								
									
										14
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: 📚 Documentation | ||||
|     url: https://github.com/MagicMirrorOrg/MagicMirror-Documentation/issues | ||||
|     about: This issue tracker is not for documentation issues. Please file documentation issues on the docs repo. | ||||
|   - name: 🤔 Support Question | ||||
|     url: https://forum.magicmirror.builders/ | ||||
|     about: Problems installing or configuring your MagicMirror? Please post your question on the MagicMirror² Forum. | ||||
|   - name: 💬 Exchange of ideas | ||||
|     url: https://discord.gg/AmGBBwPph5 | ||||
|     about: This issue tracker is not for general discussion. Please use the Discord channel. | ||||
|   - name: 📦 Issues with a 3rd-party module | ||||
|     url: https://kristjanesperanto.github.io/MagicMirror-3rd-Party-Modules/ | ||||
|     about: This issue tracker is not for 3rd-party module issues. Please file 3rd-party module issues on the module's repo. | ||||
							
								
								
									
										67
									
								
								.github/ISSUE_TEMPLATE/feature_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								.github/ISSUE_TEMPLATE/feature_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| name: 🚀 Feature Request | ||||
| description: Suggest a new feature for MagicMirror² 💡 | ||||
| title: "[Feature Request] {{ brief description }}" | ||||
| body: | ||||
|   - type: checkboxes | ||||
|     id: prerequisites | ||||
|     attributes: | ||||
|       label: Prerequisites | ||||
|       description: Please ensure you have completed all of the following. | ||||
|       options: | ||||
|         - label: I am running the latest version of MagicMirror², and know that this feature is not available now. | ||||
|           required: true | ||||
|         - label: I know my issue is not related to a third-party module. | ||||
|           required: true | ||||
|         - label: I have searched for [existing issues](https://github.com/MagicMirrorOrg/MagicMirror/issues) that already include this feature request, without success. | ||||
|           required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: description | ||||
|     attributes: | ||||
|       label: Describe the Feature Request | ||||
|       description: A clear and concise description of what the feature does. | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: use-case | ||||
|     attributes: | ||||
|       label: Describe the Use Case | ||||
|       description: A clear and concise use case for what problem this feature would solve. | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     id: proposed-solution | ||||
|     attributes: | ||||
|       label: Describe Preferred Solution | ||||
|       description: A clear and concise description of how you want this feature to be added to MagicMirror². | ||||
|  | ||||
|   - type: textarea | ||||
|     id: alternatives-considered | ||||
|     attributes: | ||||
|       label: Describe Alternatives | ||||
|       description: A clear and concise description of any alternative solutions or features you have considered. | ||||
|  | ||||
|   - type: textarea | ||||
|     id: related-code | ||||
|     attributes: | ||||
|       label: Related Code | ||||
|       description: If you are able to illustrate the feature request with an example, please provide a sample here. | ||||
|  | ||||
|   - type: textarea | ||||
|     id: additional-information | ||||
|     attributes: | ||||
|       label: Additional Information | ||||
|       description: List any other information that is relevant to your issue. Related issues, suggestions on how to implement, Stack Overflow links, forum links, etc. | ||||
|  | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
|       label: Participation | ||||
|       options: | ||||
|         - label: I am willing to submit a pull request for this change. | ||||
|           required: false | ||||
|  | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: Please **do not** open a pull request until this issue has been accepted by the team. | ||||
							
								
								
									
										27
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,24 +1,19 @@ | ||||
| Hello and thank you for wanting to contribute to the MagicMirror project | ||||
| Hello and thank you for wanting to contribute to the MagicMirror² project! | ||||
|  | ||||
| **Please make sure that you have followed these 4 rules before submitting your Pull Request:** | ||||
|  | ||||
| > 1) Base your pull requests against the `develop` branch. | ||||
| > 1. Base your pull requests against the `develop` branch. | ||||
| > 2. Include these infos in the description: | ||||
| > | ||||
| > - Does the pull request solve a **related** issue? | ||||
| > - If so, can you reference the issue like this `Fixes #<issue_number>`? | ||||
| > - What does the pull request accomplish? Use a list if needed. | ||||
| > - If it includes major visual changes please add screenshots. | ||||
| > | ||||
| > 2) Include these infos in the description: | ||||
| >  * Does the pull request solve a **related** issue? | ||||
| >  * If so, can you reference the issue like this `Fixes #<issue_number>`? | ||||
| >  * What does the pull request accomplish? Use a list if needed. | ||||
| >  * If it includes major visual changes please add screenshots. | ||||
| > | ||||
| > | ||||
| > 3) Please run `npm run lint:prettier` before submitting so that | ||||
| > style issues are fixed. | ||||
| > | ||||
| > | ||||
| > 4) Don't forget to add an entry about your changes to | ||||
| > the CHANGELOG.md file. | ||||
|  | ||||
| > 3. Please run `node --run lint:prettier` before submitting so that | ||||
| >    style issues are fixed. | ||||
| > 4. Don't forget to add an entry about your changes to | ||||
| >    the CHANGELOG.md file. | ||||
|  | ||||
| **Note**: Sometimes the development moves very fast. It is highly | ||||
| recommended that you update your branch of `develop` before creating a | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/codecov.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/codecov.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +0,0 @@ | ||||
| coverage: | ||||
|   status: | ||||
|     project: | ||||
|       default: | ||||
|         # advanced settings | ||||
|         informational: true | ||||
							
								
								
									
										20
									
								
								.github/dependabot.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/dependabot.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
|     target-branch: "develop" | ||||
|     labels: | ||||
|       - "Skip Changelog" | ||||
|       - "dependencies" | ||||
|  | ||||
|   - package-ecosystem: "npm" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "monthly" | ||||
|     target-branch: "develop" | ||||
|     labels: | ||||
|       - "Skip Changelog" | ||||
|       - "dependencies" | ||||
|       - "javascript" | ||||
							
								
								
									
										
											BIN
										
									
								
								.github/header.psd
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.github/header.psd
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										19
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,19 +0,0 @@ | ||||
| # Number of days of inactivity before an issue becomes stale | ||||
| daysUntilStale: 60 | ||||
| # Number of days of inactivity before a stale issue is closed | ||||
| daysUntilClose: 7 | ||||
| # Issues with these labels will never be considered stale | ||||
| exemptLabels: | ||||
|   - pinned | ||||
|   - security | ||||
|   - under investigation | ||||
|   - pr welcome | ||||
| # Label to use when marking an issue as stale | ||||
| staleLabel: wontfix | ||||
| # Comment to post when marking an issue as stale. Set to `false` to disable | ||||
| markComment: > | ||||
|   This issue has been automatically marked as stale because it has not had | ||||
|   recent activity. It will be closed if no further activity occurs. Thank you | ||||
|   for your contributions. | ||||
| # Comment to post when closing a stale issue. Set to `false` to disable | ||||
| closeComment: false | ||||
							
								
								
									
										69
									
								
								.github/workflows/automated-tests.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								.github/workflows/automated-tests.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||||
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||||
|  | ||||
| name: "Run Automated Tests" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master, develop] | ||||
|   pull_request: | ||||
|     branches: [master, develop] | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   code-style-check: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 15 | ||||
|     steps: | ||||
|       - name: "Checkout code" | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: "Use Node.js" | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: lts/* | ||||
|           cache: "npm" | ||||
|       - name: "Install dependencies" | ||||
|         run: | | ||||
|           node --run install-mm:dev | ||||
|       - name: "Run linter tests" | ||||
|         run: | | ||||
|           node --run test:prettier | ||||
|           node --run test:js | ||||
|           node --run test:css | ||||
|           node --run test:markdown | ||||
|   test: | ||||
|     runs-on: ubuntu-24.04 | ||||
|     timeout-minutes: 30 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [22.14.0, 22.x, 24.x] | ||||
|     steps: | ||||
|       - name: Install electron dependencies and labwc | ||||
|         run: | | ||||
|           sudo apt-get update | ||||
|           sudo apt-get install -y libnss3 libasound2t64 labwc | ||||
|       - name: "Checkout code" | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: "Use Node.js ${{ matrix.node-version }}" | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: ${{ matrix.node-version }} | ||||
|           check-latest: true | ||||
|           cache: "npm" | ||||
|       - name: "Install MagicMirror²" | ||||
|         run: | | ||||
|           node --run install-mm:dev | ||||
|       - name: "Prepare environment for tests" | ||||
|         run: | | ||||
|           # Fix chrome-sandbox permissions: | ||||
|           sudo chown root:root ./node_modules/electron/dist/chrome-sandbox | ||||
|           sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox | ||||
|           # Start labwc | ||||
|           WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_RENDERER=pixman labwc & | ||||
|           touch css/custom.css | ||||
|       - name: "Run tests" | ||||
|         run: | | ||||
|           export WAYLAND_DISPLAY=wayland-0 | ||||
|           node --run test | ||||
							
								
								
									
										25
									
								
								.github/workflows/codecov-test-suites.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/codecov-test-suites.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,25 +0,0 @@ | ||||
| # This workflow runs the automated test and uploads the coverage results to codecov.io | ||||
|  | ||||
| name: "Run Codecov Tests" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master, develop ] | ||||
|   pull_request: | ||||
|     branches: [ master, develop ] | ||||
|  | ||||
| jobs: | ||||
|   run-and-upload-coverage-report: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 30 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - run: | | ||||
|           Xvfb :99 -screen 0 1024x768x16 & | ||||
|           export DISPLAY=:99 | ||||
|           npm ci | ||||
|           npm run test:coverage | ||||
|       - uses: codecov/codecov-action@v1 | ||||
|         with: | ||||
|           file: ./coverage/lcov.info | ||||
|           fail_ci_if_error: true | ||||
							
								
								
									
										18
									
								
								.github/workflows/dep-review.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/dep-review.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # This workflow scans your pull requests for dependency changes, and will raise an error if any vulnerabilities or invalid licenses are being introduced. | ||||
| # For more information see: https://github.com/actions/dependency-review-action | ||||
|  | ||||
| name: "Review Dependencies" | ||||
|  | ||||
| on: [pull_request] | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   dependency-review: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: "Checkout code" | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: "Dependency Review" | ||||
|         uses: actions/dependency-review-action@v4 | ||||
							
								
								
									
										32
									
								
								.github/workflows/electron-rebuild.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/electron-rebuild.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| name: "Electron Rebuild Testing" | ||||
|  | ||||
| on: [pull_request] | ||||
|  | ||||
| jobs: | ||||
|   rebuild: | ||||
|     name: Run electron-rebuild | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [22.14.0, 22.x, 24.x] | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: "Use Node.js ${{ matrix.node-version }}" | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: ${{ matrix.node-version }} | ||||
|           check-latest: true | ||||
|       - name: Install MagicMirror | ||||
|         run: node --run install-mm | ||||
|       - name: Install @electron/rebuild | ||||
|         run: npm install @electron/rebuild | ||||
|       - name: Install node-libgpiod deps | ||||
|         run: | | ||||
|           sudo apt-get update | ||||
|           sudo apt-get install gpiod libgpiod2 libgpiod-dev | ||||
|       - name: Install test library (node-libgpiod) to be rebuilded | ||||
|         run: npm install node-libgpiod | ||||
|       - name: Run electron-rebuild | ||||
|         run: npx electron-rebuild | ||||
|         continue-on-error: false | ||||
							
								
								
									
										18
									
								
								.github/workflows/enforce-changelog.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/enforce-changelog.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,18 +0,0 @@ | ||||
| # This workflow enforces the update of a changelog file on every pull request | ||||
|  | ||||
| name: "Enforce Changelog" | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
|       types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] | ||||
|  | ||||
| jobs: | ||||
|   check: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 10 | ||||
|     steps: | ||||
|     - 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_target: | ||||
|     types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] | ||||
|  | ||||
| jobs: | ||||
|   check: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 10 | ||||
|     steps: | ||||
|       - name: "Enforce changelog" | ||||
|         uses: dangoslen/changelog-enforcer@v3 | ||||
|         with: | ||||
|           changeLogPath: "CHANGELOG.md" | ||||
|           skipLabels: "Skip Changelog" | ||||
|       - name: "Enforce develop branch" | ||||
|         if: ${{ github.event.pull_request.base.ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }} | ||||
|         run: | | ||||
|           echo "This PR is based against the master branch and not a release or hotfix." | ||||
|           echo "Please don't do this. Switch the branch to 'develop'." | ||||
|           exit 1 | ||||
|         env: | ||||
|           BASE_BRANCH: ${{ github.event.pull_request.base.ref }} | ||||
							
								
								
									
										33
									
								
								.github/workflows/node-ci.js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/node-ci.js.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,33 +0,0 @@ | ||||
| # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||||
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||||
|  | ||||
| name: "Run Automated Tests" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master, develop ] | ||||
|   pull_request: | ||||
|     branches: [ master, develop ] | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: ubuntu-latest | ||||
|     timeout-minutes: 30 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [12.x, 14.x, 16.x] | ||||
|     steps: | ||||
|     - 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:unit | ||||
|         npm run test:e2e | ||||
							
								
								
									
										31
									
								
								.github/workflows/spellcheck.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/spellcheck.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| # This workflow will run a spellcheck on the codebase. | ||||
| # It runs a few days before each release. At 00:00 on day-of-month 27 in March, June, September, and December. | ||||
|  | ||||
| name: Run Spellcheck | ||||
|  | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: "0 0 27 3,6,9,12 *" | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   spellcheck: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           ref: develop | ||||
|       - name: Set up Node.js | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: lts/* | ||||
|           check-latest: true | ||||
|           cache: "npm" | ||||
|       - name: Install dependencies | ||||
|         run: | | ||||
|           node --run install-mm:dev | ||||
|       - name: Run Spellcheck | ||||
|         run: node --run test:spelling | ||||
							
								
								
									
										22
									
								
								.github/workflows/stale.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/stale.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| name: "Close stale issues and PRs" | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: # needed for manually running this workflow | ||||
|   schedule: | ||||
|     - cron: "30 1 * * 6" # every Saturday at 1:30 | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
|  | ||||
| jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v9 | ||||
|         with: | ||||
|           stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions." | ||||
|           days-before-issue-stale: 60 | ||||
|           days-before-issue-close: 7 | ||||
|           operations-per-run: 100 | ||||
|           stale-issue-label: "wontfix" | ||||
|           exempt-issue-labels: "pinned,security,under investigation,pr welcome,ready (coming with next release)" | ||||
							
								
								
									
										18
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -10,13 +10,10 @@ coverage | ||||
| .lock-wscript | ||||
| build/Release | ||||
| /node_modules/**/* | ||||
| fonts/node_modules/**/* | ||||
| vendor/node_modules/**/* | ||||
| !/tests/node_modules/**/* | ||||
| jspm_modules | ||||
| .npm | ||||
| .node_repl_history | ||||
| .nyc_output/ | ||||
|  | ||||
| # Visual Studio Code ignoramuses. | ||||
| .vscode/ | ||||
| @@ -64,8 +61,12 @@ Temporary Items | ||||
| !/modules/default/** | ||||
| !/modules/README.md** | ||||
|  | ||||
| # Ignore changes to the custom css files. | ||||
| /css/custom.css | ||||
| # Ignore changes to the custom css files but keep the sample and main. | ||||
| /css/* | ||||
| !/css/custom.css.sample | ||||
| !/css/main.css | ||||
| !/css/roboto.css | ||||
| !/css/font-awesome.css | ||||
|  | ||||
| # Ignore users config file but keep the sample. | ||||
| /config/* | ||||
| @@ -80,3 +81,10 @@ Temporary Items | ||||
| *.orig | ||||
| *.rej | ||||
| *.bak | ||||
|  | ||||
| # Ignore positions file (#3518) | ||||
| js/positions.js | ||||
|  | ||||
| # Ignore lock files other than package-lock.json | ||||
| pnpm-lock.yaml | ||||
| yarn.lock | ||||
|   | ||||
							
								
								
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +0,0 @@ | ||||
| _ | ||||
| @@ -1,4 +1,5 @@ | ||||
| #!/bin/sh | ||||
| . "$(dirname "$0")/_/husky.sh" | ||||
|  | ||||
| npm run lint:staged | ||||
| if command -v npx &> /dev/null; then | ||||
|   npx lint-staged | ||||
| fi | ||||
|   | ||||
							
								
								
									
										6
									
								
								.markdownlint.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.markdownlint.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
| 	"line_length": false, | ||||
| 	"no-duplicate-heading": false, | ||||
| 	"no-inline-html": false, | ||||
| 	"no-trailing-punctuation": false | ||||
| } | ||||
| @@ -1,8 +1,8 @@ | ||||
| *.js | ||||
| *.mjs | ||||
| .husky/pre-commit | ||||
| .prettierignore | ||||
| /config | ||||
| /coverage | ||||
| /vendor | ||||
| !/vendor/vendor.js | ||||
| .github | ||||
| .nyc_output | ||||
| package-lock.json | ||||
| *.ts | ||||
| **.ics | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| { | ||||
| 	"trailingComma": "none" | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| { | ||||
| 	"extends": ["stylelint-prettier/recommended"], | ||||
| 	"plugins": ["stylelint-prettier"], | ||||
| 	"rules": { | ||||
| 		"prettier/prettier": true | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										994
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										994
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										67
									
								
								Collaboration.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Collaboration.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| # Collaboration | ||||
|  | ||||
| This document describes how collaborators of this repository should work together. | ||||
|  | ||||
| ## Pull Requests | ||||
|  | ||||
| - never merge your own PR's | ||||
| - never merge without someone having approved (approving and merging from same person is allowed) | ||||
| - wait for all approvals requested (or the author decides something different in the comments) | ||||
| - merge to `master` only for releases or other urgent issues (update notification is only triggered by tags) | ||||
| - merges to master should be tagged with the "mastermerge" label so that the test runs through | ||||
|  | ||||
| ## Issues | ||||
|  | ||||
| - "real" Issues are closed if the problem is solved and the fix is released | ||||
| - unrelated Issues (e.g. related to a foreign module) are closed immediately with a comment to open an issue in the module repository or to discuss this further in the forum or discord | ||||
|  | ||||
| ## Releases | ||||
|  | ||||
| Are done by | ||||
|  | ||||
| - [ ] @rejas | ||||
| - [ ] @sdetweil | ||||
| - [ ] @khassel | ||||
|  | ||||
| ### Pre-Deployment steps | ||||
|  | ||||
| - [ ] update dependencies (a few days before) | ||||
|  | ||||
| ### Deployment steps | ||||
|  | ||||
| - [ ] pull latest `develop` branch | ||||
| - [ ] create `prep-release` branch from `develop` | ||||
|   - [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0` | ||||
|   - [ ] test `prep-release` branch | ||||
|   - [ ] update `CHANGELOG.md` | ||||
|     - [ ] add all contributor names: `...` | ||||
|     - [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.14.0` or higher | ||||
|     - [ ] check release link at the bottom of the file | ||||
|   - [ ] commit and push all changes | ||||
|   - [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0` | ||||
|   - [ ] after successful test run via github actions: merge pull request to `develop` | ||||
| - [ ] after successful test run via github actions: create pull request from `develop` to `master` branch | ||||
|   - [ ] add label `mastermerge` | ||||
|   - [ ] title of the PR is `Release 2.xx.0` | ||||
|   - [ ] description of the PR is the section of the `CHANGELOG.md` | ||||
| - [ ] after PR tests run without issues, merge PR | ||||
| - [ ] create new release with | ||||
|   - [ ] corresponding version tag `v2.xx.0` | ||||
|   - [ ] a release name: `...` | ||||
|   - [ ] description of the release is the section of the `CHANGELOG.md` | ||||
|  | ||||
| ### Draft new development release | ||||
|  | ||||
| - [ ] checkout `develop` branch | ||||
| - [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0-develop` | ||||
| - [ ] draft new section in `CHANGELOG.md` | ||||
|   - [ ] create new release link at the bottom of the file | ||||
| - [ ] commit and push `develop` branch | ||||
| - [ ] if new release will be in January, update the year in LICENSE.md | ||||
|  | ||||
| ### After release | ||||
|  | ||||
| - [ ] publish release notes with link to github release on forum in new locked topic | ||||
| - [ ] close all issues with label `ready (coming with next release)` | ||||
| - [ ] release new documentation by merging `develop` on `master` in documentation repository | ||||
| - [ ] publish new version on [npm](https://www.npmjs.com/package/magicmirror) | ||||
| @@ -1,6 +1,6 @@ | ||||
| # The MIT License (MIT) | ||||
|  | ||||
| Copyright © 2016-2021 Michael Teeuw | ||||
| Copyright © 2016-2025 Michael Teeuw | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person | ||||
| obtaining a copy of this software and associated documentation | ||||
|   | ||||
							
								
								
									
										26
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,15 +1,17 @@ | ||||
|  | ||||
| #  | ||||
|  | ||||
| <p style="text-align: center"> | ||||
| 	<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a> | ||||
| 	<a href="https://david-dm.org/MichMich/MagicMirror?type=dev"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a> | ||||
| 	<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge" alt="CLI Best Practices"></a> | ||||
| 	<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a> | ||||
| 	<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a> | ||||
| 	<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a> | ||||
|   <a href="https://choosealicense.com/licenses/mit"> | ||||
|   <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"> | ||||
|  </a> | ||||
|  <img src="https://img.shields.io/github/actions/workflow/status/magicmirrororg/magicmirror/automated-tests.yaml" alt="GitHub Actions"> | ||||
|  <img src="https://img.shields.io/github/check-runs/magicmirrororg/magicmirror/master" alt="Build Status"> | ||||
|  <a href="https://github.com/MagicMirrorOrg/MagicMirror"> | ||||
|   <img src="https://img.shields.io/github/stars/magicmirrororg/magicmirror?style=social" alt="GitHub Stars"> | ||||
|  </a> | ||||
| </p> | ||||
|  | ||||
| **MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors). | ||||
| **MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MagicMirrorOrg/MagicMirror/graphs/contributors). | ||||
|  | ||||
| MagicMirror² focuses on a modular plugin system and uses [Electron](https://www.electronjs.org/) as an application wrapper. So no more web server or browser installs necessary! | ||||
|  | ||||
| @@ -22,7 +24,7 @@ For the full documentation including **[installation instructions](https://docs. | ||||
| - Website: [https://magicmirror.builders](https://magicmirror.builders) | ||||
| - Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders) | ||||
| - Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders) | ||||
|   - Technical discussions: https://forum.magicmirror.builders/category/11/core-system | ||||
|   - Technical discussions: <https://forum.magicmirror.builders/category/11/core-system> | ||||
| - Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx) | ||||
| - Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror) | ||||
| - Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate) | ||||
| @@ -35,11 +37,11 @@ Contributions of all kinds are welcome, not only in the form of code but also wi | ||||
| - documentation | ||||
| - translations | ||||
|  | ||||
| For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html) | ||||
| For the full contribution guidelines, check out: [https://docs.magicmirror.builders/about/contributing.html](https://docs.magicmirror.builders/about/contributing.html) | ||||
|  | ||||
| ## Enjoying MagicMirror? Consider a donation! | ||||
|  | ||||
| MagicMirror² is opensource and free. That doesn't mean we don't need any money. | ||||
| MagicMirror² is Open Source and free. That doesn't mean we don't need any money. | ||||
|  | ||||
| Please consider a donation to help us cover the ongoing costs like webservers and email services. | ||||
| If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core. | ||||
| @@ -47,5 +49,5 @@ If we receive enough donations we might even be able to free up some working hou | ||||
| To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link. | ||||
|  | ||||
| <p style="text-align: center"> | ||||
| 	<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a> | ||||
|   <a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a> | ||||
| </p> | ||||
|   | ||||
| @@ -7,16 +7,16 @@ | ||||
| 	/** | ||||
| 	 * Helper function to get server address/hostname from either the commandline or env | ||||
| 	 */ | ||||
| 	function getServerAddress() { | ||||
| 	function getServerAddress () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Get command line parameters | ||||
| 		 * Assumes that a cmdline parameter is defined with `--key [value]` | ||||
| 		 * | ||||
| 		 * @param {string} key key to look for at the command line | ||||
| 		 * @param {string} defaultValue value if no key is given at the command line | ||||
| 		 * @returns {string} the value of the parameter | ||||
| 		 */ | ||||
| 		function getCommandLineParameter(key, defaultValue = undefined) { | ||||
| 		function getCommandLineParameter (key, defaultValue = undefined) { | ||||
| 			const index = process.argv.indexOf(`--${key}`); | ||||
| 			const value = index > -1 ? process.argv[index + 1] : undefined; | ||||
| 			return value !== undefined ? String(value) : defaultValue; | ||||
| @@ -28,20 +28,19 @@ | ||||
| 		}); | ||||
|  | ||||
| 		// determine if "--use-tls"-flag was provided | ||||
| 		config["tls"] = process.argv.indexOf("--use-tls") > 0; | ||||
| 		config.tls = process.argv.indexOf("--use-tls") > 0; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Gets the config from the specified server url | ||||
| 	 * | ||||
| 	 * @param {string} url location where the server is running. | ||||
| 	 * @returns {Promise} the config | ||||
| 	 */ | ||||
| 	function getServerConfig(url) { | ||||
| 	function getServerConfig (url) { | ||||
| 		// Return new pending promise | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			// Select http or https module, depending on requested url | ||||
| 			const lib = url.startsWith("https") ? require("https") : require("http"); | ||||
| 			const lib = url.startsWith("https") ? require("node:https") : require("node:http"); | ||||
| 			const request = lib.get(url, (response) => { | ||||
| 				let configData = ""; | ||||
|  | ||||
| @@ -63,11 +62,10 @@ | ||||
|  | ||||
| 	/** | ||||
| 	 * Print a message to the console in case of errors | ||||
| 	 * | ||||
| 	 * @param {string} message error message to print | ||||
| 	 * @param {number} code error code for the exit call | ||||
| 	 */ | ||||
| 	function fail(message, code = 1) { | ||||
| 	function fail (message, code = 1) { | ||||
| 		if (message !== undefined && typeof message === "string") { | ||||
| 			console.log(message); | ||||
| 		} else { | ||||
| @@ -85,8 +83,20 @@ | ||||
| 	if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) { | ||||
| 		getServerConfig(`${prefix}${config.address}:${config.port}/config/`) | ||||
| 			.then(function (configReturn) { | ||||
| 				// check environment for DISPLAY or WAYLAND_DISPLAY | ||||
| 				const elecParams = ["js/electron.js"]; | ||||
| 				if (process.env.WAYLAND_DISPLAY) { | ||||
| 					console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`); | ||||
| 					elecParams.push("--enable-features=UseOzonePlatform"); | ||||
| 					elecParams.push("--ozone-platform=wayland"); | ||||
| 				} else if (process.env.DISPLAY) { | ||||
| 					console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`); | ||||
| 				} else { | ||||
| 					fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided."); | ||||
| 				} | ||||
| 				// Pass along the server config via an environment variable | ||||
| 				const env = Object.create(process.env); | ||||
| 				env.clientonly = true; // set to pass to electron.js | ||||
| 				const options = { env: env }; | ||||
| 				configReturn.address = config.address; | ||||
| 				configReturn.port = config.port; | ||||
| @@ -95,7 +105,7 @@ | ||||
|  | ||||
| 				// Spawn electron application | ||||
| 				const electron = require("electron"); | ||||
| 				const child = require("child_process").spawn(electron, ["js/electron.js"], options); | ||||
| 				const child = require("node:child_process").spawn(electron, elecParams, options); | ||||
|  | ||||
| 				// Pipe all child process output to current stdout | ||||
| 				child.stdout.on("data", function (buf) { | ||||
| @@ -123,4 +133,4 @@ | ||||
| 	} else { | ||||
| 		fail(); | ||||
| 	} | ||||
| })(); | ||||
| }()); | ||||
|   | ||||
| @@ -1,41 +1,41 @@ | ||||
| /* Magic Mirror Config Sample | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
| /* Config Sample | ||||
|  * | ||||
|  * For more information on how you can configure this file | ||||
|  * see https://docs.magicmirror.builders/getting-started/configuration.html#general | ||||
|  * see https://docs.magicmirror.builders/configuration/introduction.html | ||||
|  * and https://docs.magicmirror.builders/modules/configuration.html | ||||
|  * | ||||
|  * You can use environment variables using a `config.js.template` file instead of `config.js` | ||||
|  * which will be converted to `config.js` while starting. For more information | ||||
|  * see https://docs.magicmirror.builders/configuration/introduction.html#enviromnent-variables | ||||
|  */ | ||||
| let config = { | ||||
| 	address: "localhost", 	// Address to listen on, can be: | ||||
| 	address: "localhost",	// Address to listen on, can be: | ||||
| 							// - "localhost", "127.0.0.1", "::1" to listen on loopback interface | ||||
| 							// - another specific IPv4/6 to listen on a specific interface | ||||
| 							// - "0.0.0.0", "::" to listen on any interface | ||||
| 							// Default, when address config is left out or empty, is "localhost" | ||||
| 	port: 8080, | ||||
| 	basePath: "/", 	// The URL path where MagicMirror is hosted. If you are using a Reverse proxy | ||||
| 					// you must set the sub path here. basePath must end with a / | ||||
| 	ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], 	// Set [] to allow all IP addresses | ||||
| 															// or add a specific IPv4 of 192.168.1.5 : | ||||
| 															// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"], | ||||
| 															// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format : | ||||
| 															// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"], | ||||
| 	basePath: "/",	// The URL path where MagicMirror² is hosted. If you are using a Reverse proxy | ||||
| 									// you must set the sub path here. basePath must end with a / | ||||
| 	ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],	// Set [] to allow all IP addresses | ||||
| 									// or add a specific IPv4 of 192.168.1.5 : | ||||
| 									// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"], | ||||
| 									// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format : | ||||
| 									// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"], | ||||
|  | ||||
| 	useHttps: false, 		// Support HTTPS or not, default "false" will use HTTP | ||||
| 	httpsPrivateKey: "", 	// HTTPS private key path, only require when useHttps is true | ||||
| 	httpsCertificate: "", 	// HTTPS Certificate path, only require when useHttps is true | ||||
| 	useHttps: false,			// Support HTTPS or not, default "false" will use HTTP | ||||
| 	httpsPrivateKey: "",	// HTTPS private key path, only require when useHttps is true | ||||
| 	httpsCertificate: "",	// HTTPS Certificate path, only require when useHttps is true | ||||
|  | ||||
| 	language: "en", | ||||
| 	locale: "en-US", | ||||
| 	locale: "en-US",   // this variable is provided as a consistent location | ||||
| 			   // it is currently only used by 3rd party modules. no MagicMirror code uses this value | ||||
| 			   // as we have no usage, we  have no constraints on what this field holds | ||||
| 			   // see https://en.wikipedia.org/wiki/Locale_(computer_software) for the possibilities | ||||
|  | ||||
| 	logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging | ||||
| 	timeFormat: 24, | ||||
| 	units: "metric", | ||||
| 	// serverOnly:  true/false/"local" , | ||||
| 	// local for armv6l processors, default | ||||
| 	//   starts serveronly and then starts chrome browser | ||||
| 	// false, default for all NON-armv6l devices | ||||
| 	// true, force serveronly mode, because you want to.. no UI on this device | ||||
|  | ||||
| 	modules: [ | ||||
| 		{ | ||||
| @@ -56,8 +56,10 @@ let config = { | ||||
| 			config: { | ||||
| 				calendars: [ | ||||
| 					{ | ||||
| 						fetchInterval: 7 * 24 * 60 * 60 * 1000, | ||||
| 						symbol: "calendar-check", | ||||
| 						url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"					} | ||||
| 						url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics" | ||||
| 					} | ||||
| 				] | ||||
| 			} | ||||
| 		}, | ||||
| @@ -69,11 +71,10 @@ let config = { | ||||
| 			module: "weather", | ||||
| 			position: "top_right", | ||||
| 			config: { | ||||
| 				weatherProvider: "openweathermap", | ||||
| 				weatherProvider: "openmeteo", | ||||
| 				type: "current", | ||||
| 				location: "New York", | ||||
| 				locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city | ||||
| 				apiKey: "YOUR_OPENWEATHER_API_KEY" | ||||
| 				lat: 40.776676, | ||||
| 				lon: -73.971321 | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| @@ -81,11 +82,10 @@ let config = { | ||||
| 			position: "top_right", | ||||
| 			header: "Weather Forecast", | ||||
| 			config: { | ||||
| 				weatherProvider: "openweathermap", | ||||
| 				weatherProvider: "openmeteo", | ||||
| 				type: "forecast", | ||||
| 				location: "New York", | ||||
| 				locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city | ||||
| 				apiKey: "YOUR_OPENWEATHER_API_KEY" | ||||
| 				lat: 40.776676, | ||||
| 				lon: -73.971321 | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| @@ -108,4 +108,4 @@ let config = { | ||||
| }; | ||||
|  | ||||
| /*************** DO NOT EDIT THE LINE BELOW ***************/ | ||||
| if (typeof module !== "undefined") {module.exports = config;} | ||||
| if (typeof module !== "undefined") { module.exports = config; } | ||||
|   | ||||
							
								
								
									
										253
									
								
								cspell.config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								cspell.config.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,253 @@ | ||||
| { | ||||
| 	"version": "0.2", | ||||
| 	"language": "en", | ||||
| 	"words": [ | ||||
| 		"aarch", | ||||
| 		"Alvinger", | ||||
| 		"Ampio", | ||||
| 		"andrezibaia", | ||||
| 		"angeldeejay", | ||||
| 		"apiontek", | ||||
| 		"armv", | ||||
| 		"ashishtank", | ||||
| 		"autoplay", | ||||
| 		"beada", | ||||
| 		"Binney", | ||||
| 		"bluemanos", | ||||
| 		"bnitkin", | ||||
| 		"bokmål", | ||||
| 		"Brasileiro", | ||||
| 		"Brento", | ||||
| 		"browserwindow", | ||||
| 		"bryanzzhu", | ||||
| 		"btoconnor", | ||||
| 		"bugsounet", | ||||
| 		"buxxi", | ||||
| 		"byday", | ||||
| 		"calendarfetcherutils", | ||||
| 		"calendarutils", | ||||
| 		"chamakura", | ||||
| 		"cjbrunner", | ||||
| 		"clientonly", | ||||
| 		"clockfaces", | ||||
| 		"cmdline", | ||||
| 		"codac", | ||||
| 		"Crazylegstoo", | ||||
| 		"crazyscot", | ||||
| 		"Creepin", | ||||
| 		"currentweather", | ||||
| 		"CUSTOMCSS", | ||||
| 		"customregions", | ||||
| 		"cxmj", | ||||
| 		"Cymraeg", | ||||
| 		"dariom", | ||||
| 		"darksky", | ||||
| 		"dateheader", | ||||
| 		"dateheaders", | ||||
| 		"davide", | ||||
| 		"DAYAFTERTOMORROW", | ||||
| 		"DAYBEFOREYESTERDAY", | ||||
| 		"defaultmodules", | ||||
| 		"dgoth", | ||||
| 		"dkallen", | ||||
| 		"drivelist", | ||||
| 		"DTEND", | ||||
| 		"DTSTAMP", | ||||
| 		"DTSTART", | ||||
| 		"Duffman", | ||||
| 		"earlman", | ||||
| 		"easyas", | ||||
| 		"eddiehung", | ||||
| 		"Edgardos", | ||||
| 		"Ekristoffe", | ||||
| 		"elec", | ||||
| 		"eltociear", | ||||
| 		"envcanada", | ||||
| 		"envsub", | ||||
| 		"envsubst", | ||||
| 		"eouia", | ||||
| 		"exdate", | ||||
| 		"expectedheaders", | ||||
| 		"ezeholz", | ||||
| 		"Faizan", | ||||
| 		"feedme", | ||||
| 		"feelslike", | ||||
| 		"Fenner", | ||||
| 		"fewieden", | ||||
| 		"fixuppm", | ||||
| 		"flopp", | ||||
| 		"fontawesome", | ||||
| 		"fontface", | ||||
| 		"forecastweather", | ||||
| 		"fortawesome", | ||||
| 		"frameguard", | ||||
| 		"Frysk", | ||||
| 		"fulldate", | ||||
| 		"fullday", | ||||
| 		"fullscreen", | ||||
| 		"geraki", | ||||
| 		"Gevoelstemperatuur", | ||||
| 		"GHSA", | ||||
| 		"ghsas", | ||||
| 		"grenagit", | ||||
| 		"Heiko", | ||||
| 		"Hirschberger", | ||||
| 		"hourlyweather", | ||||
| 		"Hwind", | ||||
| 		"ical", | ||||
| 		"illimarkangur", | ||||
| 		"Ingan", | ||||
| 		"ipfilter", | ||||
| 		"ismarslomic", | ||||
| 		"jakemulley", | ||||
| 		"jakobsarwary", | ||||
| 		"jalibu", | ||||
| 		"jargordon", | ||||
| 		"jetson", | ||||
| 		"jkriegshauser", | ||||
| 		"jsdocs", | ||||
| 		"jsonlint", | ||||
| 		"jupadin", | ||||
| 		"kaennchenstruggle", | ||||
| 		"Kalenderwoche", | ||||
| 		"kenzal", | ||||
| 		"Keyport", | ||||
| 		"khassel", | ||||
| 		"Kingdon", | ||||
| 		"kioskmode", | ||||
| 		"klaernie", | ||||
| 		"kleinmantara", | ||||
| 		"Kmph", | ||||
| 		"Knapoc", | ||||
| 		"Koepke", | ||||
| 		"kolbyjack", | ||||
| 		"krekos", | ||||
| 		"Kristjan", | ||||
| 		"krukle", | ||||
| 		"labwc", | ||||
| 		"Landis", | ||||
| 		"larryare", | ||||
| 		"letsencrypt", | ||||
| 		"libgpiod", | ||||
| 		"Lightspeed", | ||||
| 		"locationforecast", | ||||
| 		"lockstring", | ||||
| 		"lstrip", | ||||
| 		"Luciella", | ||||
| 		"luxon", | ||||
| 		"lxsession", | ||||
| 		"magicmirror", | ||||
| 		"martingron", | ||||
| 		"marvai", | ||||
| 		"mastermerge", | ||||
| 		"matchtype", | ||||
| 		"maxentries", | ||||
| 		"Meteo", | ||||
| 		"michaelteeuw", | ||||
| 		"michmich", | ||||
| 		"Midori", | ||||
| 		"mirontoli", | ||||
| 		"MISSINGLANG", | ||||
| 		"mixasgr", | ||||
| 		"MMPM", | ||||
| 		"modernizr", | ||||
| 		"modulename", | ||||
| 		"multiday", | ||||
| 		"Mystara", | ||||
| 		"Ñandú", | ||||
| 		"nathannaveen", | ||||
| 		"naveensrinivasan", | ||||
| 		"ndom", | ||||
| 		"Nerfzooka", | ||||
| 		"NEWSFEED", | ||||
| 		"newsitems", | ||||
| 		"nfogal", | ||||
| 		"njwilliams", | ||||
| 		"nonrepeating", | ||||
| 		"Norsk", | ||||
| 		"nunjuck", | ||||
| 		"odroid", | ||||
| 		"oemel", | ||||
| 		"onecall", | ||||
| 		"onevent", | ||||
| 		"openmeteo", | ||||
| 		"openweathermap", | ||||
| 		"oraclesean", | ||||
| 		"oscarb", | ||||
| 		"philnagel", | ||||
| 		"Português", | ||||
| 		"PRECIP", | ||||
| 		"Problema", | ||||
| 		"psieg", | ||||
| 		"radokristof", | ||||
| 		"rajniszp", | ||||
| 		"rebuilded", | ||||
| 		"Reis", | ||||
| 		"rejas", | ||||
| 		"Resig", | ||||
| 		"roboto", | ||||
| 		"rohitdharavath", | ||||
| 		"Rosso", | ||||
| 		"rrule", | ||||
| 		"savvadam", | ||||
| 		"sdetweil", | ||||
| 		"sendheaders", | ||||
| 		"serveronly", | ||||
| 		"sexualized", | ||||
| 		"skpanagiotis", | ||||
| 		"SMHI", | ||||
| 		"Snille", | ||||
| 		"socketclient", | ||||
| 		"socketio", | ||||
| 		"spectron", | ||||
| 		"Starinvest", | ||||
| 		"sthuber", | ||||
| 		"Stieber", | ||||
| 		"stylelintrc", | ||||
| 		"subclassing", | ||||
| 		"sunaction", | ||||
| 		"suncalc", | ||||
| 		"suntimes", | ||||
| 		"symboltest", | ||||
| 		"systeminformation", | ||||
| 		"tada", | ||||
| 		"taglist", | ||||
| 		"Teeuw", | ||||
| 		"TESTMODE", | ||||
| 		"thomasrockhu", | ||||
| 		"tomzt", | ||||
| 		"ukmetoffice", | ||||
| 		"ukmetofficedatahub", | ||||
| 		"unitless", | ||||
| 		"unparseable", | ||||
| 		"updatenotification", | ||||
| 		"Vaice", | ||||
| 		"veeck", | ||||
| 		"VEVENT", | ||||
| 		"vgtu", | ||||
| 		"Voelt", | ||||
| 		"vppencilsharpener", | ||||
| 		"Wallys", | ||||
| 		"Weatherbit", | ||||
| 		"WEATHERDATA", | ||||
| 		"Weatherflow", | ||||
| 		"weatherforecast", | ||||
| 		"weathergov", | ||||
| 		"weathericons", | ||||
| 		"weatherobject", | ||||
| 		"weatherutils", | ||||
| 		"windspeed", | ||||
| 		"Woolridge", | ||||
| 		"worktree", | ||||
| 		"xlarge", | ||||
| 		"xrandr", | ||||
| 		"xsmall", | ||||
| 		"xsorifc", | ||||
| 		"xwindows", | ||||
| 		"xxxe", | ||||
| 		"Ybbet", | ||||
| 		"yearmatchgroup" | ||||
| 	], | ||||
| 	"ignorePaths": ["node_modules/**", "modules/**", "translations/**", "tests/mocks/**", "tests/e2e/modules/clock_es_spec.js", "css/roboto.css"], | ||||
| 	"dictionaries": ["node"] | ||||
| } | ||||
| @@ -1,10 +1,8 @@ | ||||
| /* Magic Mirror Custom CSS Sample | ||||
| /* Custom CSS Sample | ||||
|  * | ||||
|  * Change color and fonts here. | ||||
|  * | ||||
|  * Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;' | ||||
|  * | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
|  | ||||
| /* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */ | ||||
| @@ -18,7 +16,7 @@ | ||||
|  | ||||
|   --font-primary: "Roboto Condensed"; | ||||
|   --font-secondary: "Roboto"; | ||||
|    | ||||
|  | ||||
|   --font-size: 20px; | ||||
|   --font-size-small: 0.75rem; | ||||
|  | ||||
| @@ -26,6 +24,6 @@ | ||||
|   --gap-body-right: 60px; | ||||
|   --gap-body-bottom: 60px; | ||||
|   --gap-body-left: 60px; | ||||
|    | ||||
|  | ||||
|   --gap-modules: 30px; | ||||
| } | ||||
|   | ||||
| @@ -1,2 +1,2 @@ | ||||
| @import url("../node_modules/@fortawesome/fontawesome-free/css/all.min.css"); | ||||
| @import url("../node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css"); | ||||
| @import url("../node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css"); | ||||
							
								
								
									
										52
									
								
								css/main.css
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								css/main.css
									
									
									
									
									
								
							| @@ -3,18 +3,18 @@ | ||||
|   --color-text-dimmed: #666; | ||||
|   --color-text-bright: #fff; | ||||
|   --color-background: #000; | ||||
|  | ||||
|   --font-primary: "Roboto Condensed"; | ||||
|   --font-secondary: "Roboto"; | ||||
|  | ||||
|   --font-size: 20px; | ||||
|   --font-size-small: 0.75rem; | ||||
|  | ||||
|   --font-size-xsmall: 0.75rem; | ||||
|   --font-size-small: 1rem; | ||||
|   --font-size-medium: 1.5rem; | ||||
|   --font-size-large: 3.25rem; | ||||
|   --font-size-xlarge: 3.75rem; | ||||
|   --gap-body-top: 60px; | ||||
|   --gap-body-right: 60px; | ||||
|   --gap-body-bottom: 60px; | ||||
|   --gap-body-left: 60px; | ||||
|  | ||||
|   --gap-modules: 30px; | ||||
| } | ||||
|  | ||||
| @@ -60,27 +60,27 @@ body { | ||||
| } | ||||
|  | ||||
| .xsmall { | ||||
|   font-size: var(--font-size-small); | ||||
|   font-size: var(--font-size-xsmall); | ||||
|   line-height: 1.275; | ||||
| } | ||||
|  | ||||
| .small { | ||||
|   font-size: 1rem; | ||||
|   font-size: var(--font-size-small); | ||||
|   line-height: 1.25; | ||||
| } | ||||
|  | ||||
| .medium { | ||||
|   font-size: 1.5rem; | ||||
|   font-size: var(--font-size-medium); | ||||
|   line-height: 1.225; | ||||
| } | ||||
|  | ||||
| .large { | ||||
|   font-size: 3.25rem; | ||||
|   font-size: var(--font-size-large); | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| .xlarge { | ||||
|   font-size: 3.75rem; | ||||
|   font-size: var(--font-size-xlarge); | ||||
|   line-height: 1; | ||||
|   letter-spacing: -3px; | ||||
| } | ||||
| @@ -115,7 +115,7 @@ body { | ||||
|  | ||||
| header { | ||||
|   text-transform: uppercase; | ||||
|   font-size: var(--font-size-small); | ||||
|   font-size: var(--font-size-xsmall); | ||||
|   font-family: var(--font-primary), Arial, Helvetica, sans-serif; | ||||
|   font-weight: 400; | ||||
|   border-bottom: 1px solid var(--color-text-dimmed); | ||||
| @@ -138,6 +138,14 @@ sup { | ||||
|   margin-bottom: var(--gap-modules); | ||||
| } | ||||
|  | ||||
| .module.hidden { | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .module:not(.hidden) { | ||||
|   pointer-events: auto; | ||||
| } | ||||
|  | ||||
| .region.bottom .module { | ||||
|   margin-top: var(--gap-modules); | ||||
|   margin-bottom: 0; | ||||
| @@ -163,17 +171,10 @@ sup { | ||||
|  | ||||
| .region.fullscreen { | ||||
|   position: absolute; | ||||
|   top: calc(-1 * var(--gap-body-top)); | ||||
|   left: calc(-1 * var(--gap-body-left)); | ||||
|   right: calc(-1 * var(--gap-body-right)); | ||||
|   bottom: calc(-1 * var(--gap-body-bottom)); | ||||
|   inset: calc(-1 * var(--gap-body-top)) calc(-1 * var(--gap-body-right)) calc(-1 * var(--gap-body-bottom)) calc(-1 * var(--gap-body-left)); | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .region.fullscreen * { | ||||
|   pointer-events: auto; | ||||
| } | ||||
|  | ||||
| .region.right { | ||||
|   right: 0; | ||||
|   text-align: right; | ||||
| @@ -238,3 +239,16 @@ sup { | ||||
|   border-spacing: 0; | ||||
|   border-collapse: separate; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Container Definitions. | ||||
|  */ | ||||
|  | ||||
| .region .container { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
|  | ||||
| .region .container.hidden { | ||||
|   display: none; | ||||
| } | ||||
|   | ||||
							
								
								
									
										671
									
								
								css/roboto.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										671
									
								
								css/roboto.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,671 @@ | ||||
| /* roboto-cyrillic-ext-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-100-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 100; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-300-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-400-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-500-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 500; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-cyrillic-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-greek-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-vietnamese-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-latin-700-normal */ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-vietnamese-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-ext-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-300-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 300; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-vietnamese-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-ext-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-400-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 400; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-cyrillic-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+1F00-1FFF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-greek-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0370-03FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-vietnamese-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-ext-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| /* roboto-condensed-latin-700-normal */ | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   font-weight: 700; | ||||
|   src: | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff2") format("woff2"), | ||||
|     url("../node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff") format("woff"); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
| } | ||||
							
								
								
									
										132
									
								
								eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| import {defineConfig, globalIgnores} from "eslint/config"; | ||||
| import globals from "globals"; | ||||
| import {flatConfigs as importX} from "eslint-plugin-import-x"; | ||||
| import jest from "eslint-plugin-jest"; | ||||
| import js from "@eslint/js"; | ||||
| import jsdoc from "eslint-plugin-jsdoc"; | ||||
| import packageJson from "eslint-plugin-package-json"; | ||||
| import stylistic from "@stylistic/eslint-plugin"; | ||||
|  | ||||
| export default defineConfig([ | ||||
| 	globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]), | ||||
| 	{ | ||||
| 		files: ["**/*.js"], | ||||
| 		languageOptions: { | ||||
| 			ecmaVersion: "latest", | ||||
| 			globals: { | ||||
| 				...globals.browser, | ||||
| 				...globals.node, | ||||
| 				Log: "readonly", | ||||
| 				MM: "readonly", | ||||
| 				Module: "readonly", | ||||
| 				config: "readonly", | ||||
| 				moment: "readonly" | ||||
| 			} | ||||
| 		}, | ||||
| 		plugins: {js, jsdoc, stylistic}, | ||||
| 		extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdoc.configs["flat/recommended"], "stylistic/all"], | ||||
| 		rules: { | ||||
| 			"@stylistic/array-element-newline": ["error", "consistent"], | ||||
| 			"@stylistic/arrow-parens": ["error", "always"], | ||||
| 			"@stylistic/brace-style": "off", | ||||
| 			"@stylistic/comma-dangle": ["error", "never"], | ||||
| 			"@stylistic/dot-location": ["error", "property"], | ||||
| 			"@stylistic/function-call-argument-newline": ["error", "consistent"], | ||||
| 			"@stylistic/function-paren-newline": ["error", "consistent"], | ||||
| 			"@stylistic/implicit-arrow-linebreak": ["error", "beside"], | ||||
| 			"@stylistic/indent": ["error", "tab"], | ||||
| 			"@stylistic/max-statements-per-line": ["error", {max: 2}], | ||||
| 			"@stylistic/multiline-comment-style": "off", | ||||
| 			"@stylistic/multiline-ternary": ["error", "always-multiline"], | ||||
| 			"@stylistic/newline-per-chained-call": ["error", {ignoreChainWithDepth: 4}], | ||||
| 			"@stylistic/no-extra-parens": "off", | ||||
| 			"@stylistic/no-tabs": "off", | ||||
| 			"@stylistic/object-curly-spacing": ["error", "always"], | ||||
| 			"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}], | ||||
| 			"@stylistic/operator-linebreak": ["error", "before"], | ||||
| 			"@stylistic/padded-blocks": "off", | ||||
| 			"@stylistic/quote-props": ["error", "as-needed"], | ||||
| 			"@stylistic/quotes": ["error", "double"], | ||||
| 			"@stylistic/semi": ["error", "always"], | ||||
| 			"@stylistic/space-before-function-paren": ["error", "always"], | ||||
| 			"@stylistic/spaced-comment": "off", | ||||
| 			"dot-notation": "error", | ||||
| 			eqeqeq: "error", | ||||
| 			"id-length": "off", | ||||
| 			"import-x/extensions": "error", | ||||
| 			"import-x/newline-after-import": "error", | ||||
| 			"import-x/order": "error", | ||||
| 			"init-declarations": "off", | ||||
| 			"jest/consistent-test-it": "warn", | ||||
| 			"jest/no-done-callback": "warn", | ||||
| 			"jest/prefer-expect-resolves": "warn", | ||||
| 			"jest/prefer-mock-promise-shorthand": "warn", | ||||
| 			"jest/prefer-to-be": "warn", | ||||
| 			"jest/prefer-to-have-length": "warn", | ||||
| 			"max-lines-per-function": ["warn", 400], | ||||
| 			"max-statements": "off", | ||||
| 			"no-global-assign": "off", | ||||
| 			"no-inline-comments": "off", | ||||
| 			"no-magic-numbers": "off", | ||||
| 			"no-param-reassign": "error", | ||||
| 			"no-plusplus": "off", | ||||
| 			"no-prototype-builtins": "off", | ||||
| 			"no-ternary": "off", | ||||
| 			"no-throw-literal": "error", | ||||
| 			"no-undefined": "off", | ||||
| 			"no-unneeded-ternary": "error", | ||||
| 			"no-unused-vars": "off", | ||||
| 			"no-useless-return": "error", | ||||
| 			"no-warning-comments": "off", | ||||
| 			"object-shorthand": ["error", "methods"], | ||||
| 			"one-var": "off", | ||||
| 			"prefer-template": "error", | ||||
| 			"sort-keys": "off" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		files: ["**/*.js"], | ||||
| 		ignores: [ | ||||
| 			"clientonly/index.js", | ||||
| 			"modules/default/calendar/debug.js", | ||||
| 			"js/logger.js", | ||||
| 			"tests/**/*.js" | ||||
| 		], | ||||
| 		rules: {"no-console": "error"} | ||||
| 	}, | ||||
| 	{ | ||||
| 		files: ["**/package.json"], | ||||
| 		plugins: {packageJson}, | ||||
| 		extends: ["packageJson/recommended"] | ||||
| 	}, | ||||
| 	{ | ||||
| 		files: ["**/*.mjs"], | ||||
| 		languageOptions: { | ||||
| 			ecmaVersion: "latest", | ||||
| 			globals: { | ||||
| 				...globals.node | ||||
| 			}, | ||||
| 			sourceType: "module" | ||||
| 		}, | ||||
| 		plugins: {js, stylistic}, | ||||
| 		extends: [importX.recommended, "js/all", "stylistic/all"], | ||||
| 		rules: { | ||||
| 			"@stylistic/array-element-newline": "off", | ||||
| 			"@stylistic/indent": ["error", "tab"], | ||||
| 			"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}], | ||||
| 			"@stylistic/padded-blocks": ["error", "never"], | ||||
| 			"@stylistic/quote-props": ["error", "as-needed"], | ||||
| 			"import-x/no-unresolved": ["error", {ignore: ["eslint/config"]}], | ||||
| 			"max-lines-per-function": ["error", 100], | ||||
| 			"no-magic-numbers": "off", | ||||
| 			"one-var": ["error", "never"], | ||||
| 			"sort-keys": "off" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		files: ["tests/configs/modules/weather/*.js"], | ||||
| 		rules: { | ||||
| 			"@stylistic/quotes": "off" | ||||
| 		} | ||||
| 	} | ||||
| ]); | ||||
							
								
								
									
										26
									
								
								fonts/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								fonts/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,26 +0,0 @@ | ||||
| { | ||||
| 	"name": "magicmirror-fonts", | ||||
| 	"lockfileVersion": 2, | ||||
| 	"requires": true, | ||||
| 	"packages": { | ||||
| 		"": { | ||||
| 			"name": "magicmirror-fonts", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"roboto-fontface": "^0.10.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/roboto-fontface": { | ||||
| 			"version": "0.10.0", | ||||
| 			"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", | ||||
| 			"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==" | ||||
| 		} | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"roboto-fontface": { | ||||
| 			"version": "0.10.0", | ||||
| 			"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", | ||||
| 			"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==" | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,15 +0,0 @@ | ||||
| { | ||||
| 	"name": "magicmirror-fonts", | ||||
| 	"description": "Package for fonts use by MagicMirror Core.", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "git+https://github.com/MichMich/MagicMirror.git" | ||||
| 	}, | ||||
| 	"license": "MIT", | ||||
| 	"bugs": { | ||||
| 		"url": "https://github.com/MichMich/MagicMirror/issues" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"roboto-fontface": "^0.10.0" | ||||
| 	} | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-weight: 100; | ||||
|   src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-weight: 300; | ||||
|   src: local("Roboto Condensed Light"), local("RobotoCondensed-Light"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"), | ||||
|     url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local("Roboto Condensed"), local("RobotoCondensed-Regular"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"), | ||||
|     url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: "Roboto Condensed"; | ||||
|   font-style: normal; | ||||
|   font-weight: 700; | ||||
|   src: local("Roboto Condensed Bold"), local("RobotoCondensed-Bold"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"), | ||||
|     url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local("Roboto"), local("Roboto-Regular"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-weight: 500; | ||||
|   src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-weight: 700; | ||||
|   src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: Roboto; | ||||
|   font-style: normal; | ||||
|   font-weight: 300; | ||||
|   src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff"); | ||||
| } | ||||
							
								
								
									
										15
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								index.html
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| <!DOCTYPE html> | ||||
| <!doctype html> | ||||
| <html> | ||||
|   <head> | ||||
|     <title>MagicMirror²</title> | ||||
| @@ -12,11 +12,13 @@ | ||||
|  | ||||
|     <link rel="icon" href="data:;base64,iVBORw0KGgo=" /> | ||||
|     <link rel="stylesheet" type="text/css" href="css/main.css" /> | ||||
|     <link rel="stylesheet" type="text/css" href="fonts/roboto.css" /> | ||||
|     <link rel="stylesheet" type="text/css" href="css/roboto.css" /> | ||||
|     <link rel="stylesheet" type="text/css" href="node_modules/animate.css/animate.min.css" /> | ||||
|     <!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. --> | ||||
|  | ||||
|     <script type="text/javascript"> | ||||
|       var version = "#VERSION#"; | ||||
|       window.mmVersion = "#VERSION#"; | ||||
|       window.mmTestMode = "#TESTMODE#"; | ||||
|     </script> | ||||
|   </head> | ||||
|   <body> | ||||
| @@ -40,11 +42,12 @@ | ||||
|     </div> | ||||
|     <div class="region fullscreen above"><div class="container"></div></div> | ||||
|     <script type="text/javascript" src="socket.io/socket.io.js"></script> | ||||
|     <script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script> | ||||
|     <script type="text/javascript" src="node_modules/nunjucks/browser/nunjucks.min.js"></script> | ||||
|     <script type="text/javascript" src="js/defaults.js"></script> | ||||
|     <script type="text/javascript" src="#CONFIG_FILE#"></script> | ||||
|     <script type="text/javascript" src="vendor/vendor.js"></script> | ||||
|     <script type="text/javascript" src="js/vendor.js"></script> | ||||
|     <script type="text/javascript" src="modules/default/defaultmodules.js"></script> | ||||
|     <script type="text/javascript" src="modules/default/utils.js"></script> | ||||
|     <script type="text/javascript" src="js/logger.js"></script> | ||||
|     <script type="text/javascript" src="translations/translations.js"></script> | ||||
|     <script type="text/javascript" src="js/translator.js"></script> | ||||
| @@ -52,6 +55,8 @@ | ||||
|     <script type="text/javascript" src="js/module.js"></script> | ||||
|     <script type="text/javascript" src="js/loader.js"></script> | ||||
|     <script type="text/javascript" src="js/socketclient.js"></script> | ||||
|     <script type="text/javascript" src="js/animateCSS.js"></script> | ||||
|     <script type="text/javascript" src="js/positions.js"></script> | ||||
|     <script type="text/javascript" src="js/main.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| # This file is still here to keep PM2 working on older installations. | ||||
| cd ~/MagicMirror | ||||
| DISPLAY=:0 npm start | ||||
							
								
								
									
										32
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| module.exports = async () => { | ||||
| 	return { | ||||
| 		verbose: true, | ||||
| 		testTimeout: 20000, | ||||
| 		testSequencer: "<rootDir>/tests/utils/test_sequencer.js", | ||||
| 		projects: [ | ||||
| 			{ | ||||
| 				displayName: "unit", | ||||
| 				globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js", | ||||
| 				moduleNameMapper: { | ||||
| 					logger: "<rootDir>/js/logger.js" | ||||
| 				}, | ||||
| 				testMatch: ["**/tests/unit/**/*.[jt]s?(x)"], | ||||
| 				testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"] | ||||
| 			}, | ||||
| 			{ | ||||
| 				displayName: "electron", | ||||
| 				testMatch: ["**/tests/electron/**/*.[jt]s?(x)"], | ||||
| 				testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"] | ||||
| 			}, | ||||
| 			{ | ||||
| 				displayName: "e2e", | ||||
| 				testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"], | ||||
| 				modulePaths: ["<rootDir>/js/"], | ||||
| 				testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"] | ||||
| 			} | ||||
| 		], | ||||
| 		collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"], | ||||
| 		coverageReporters: ["lcov", "text"], | ||||
| 		coverageProvider: "v8" | ||||
| 	}; | ||||
| }; | ||||
							
								
								
									
										158
									
								
								js/animateCSS.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								js/animateCSS.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| /* enumeration of animations in Array **/ | ||||
| const AnimateCSSIn = [ | ||||
| 	// Attention seekers | ||||
| 	"bounce", | ||||
| 	"flash", | ||||
| 	"pulse", | ||||
| 	"rubberBand", | ||||
| 	"shakeX", | ||||
| 	"shakeY", | ||||
| 	"headShake", | ||||
| 	"swing", | ||||
| 	"tada", | ||||
| 	"wobble", | ||||
| 	"jello", | ||||
| 	"heartBeat", | ||||
| 	// Back entrances | ||||
| 	"backInDown", | ||||
| 	"backInLeft", | ||||
| 	"backInRight", | ||||
| 	"backInUp", | ||||
| 	// Bouncing entrances | ||||
| 	"bounceIn", | ||||
| 	"bounceInDown", | ||||
| 	"bounceInLeft", | ||||
| 	"bounceInRight", | ||||
| 	"bounceInUp", | ||||
| 	// Fading entrances | ||||
| 	"fadeIn", | ||||
| 	"fadeInDown", | ||||
| 	"fadeInDownBig", | ||||
| 	"fadeInLeft", | ||||
| 	"fadeInLeftBig", | ||||
| 	"fadeInRight", | ||||
| 	"fadeInRightBig", | ||||
| 	"fadeInUp", | ||||
| 	"fadeInUpBig", | ||||
| 	"fadeInTopLeft", | ||||
| 	"fadeInTopRight", | ||||
| 	"fadeInBottomLeft", | ||||
| 	"fadeInBottomRight", | ||||
| 	// Flippers | ||||
| 	"flip", | ||||
| 	"flipInX", | ||||
| 	"flipInY", | ||||
| 	// Lightspeed | ||||
| 	"lightSpeedInRight", | ||||
| 	"lightSpeedInLeft", | ||||
| 	// Rotating entrances | ||||
| 	"rotateIn", | ||||
| 	"rotateInDownLeft", | ||||
| 	"rotateInDownRight", | ||||
| 	"rotateInUpLeft", | ||||
| 	"rotateInUpRight", | ||||
| 	// Specials | ||||
| 	"jackInTheBox", | ||||
| 	"rollIn", | ||||
| 	// Zooming entrances | ||||
| 	"zoomIn", | ||||
| 	"zoomInDown", | ||||
| 	"zoomInLeft", | ||||
| 	"zoomInRight", | ||||
| 	"zoomInUp", | ||||
| 	// Sliding entrances | ||||
| 	"slideInDown", | ||||
| 	"slideInLeft", | ||||
| 	"slideInRight", | ||||
| 	"slideInUp" | ||||
| ]; | ||||
|  | ||||
| const AnimateCSSOut = [ | ||||
| 	// Back exits | ||||
| 	"backOutDown", | ||||
| 	"backOutLeft", | ||||
| 	"backOutRight", | ||||
| 	"backOutUp", | ||||
| 	// Bouncing exits | ||||
| 	"bounceOut", | ||||
| 	"bounceOutDown", | ||||
| 	"bounceOutLeft", | ||||
| 	"bounceOutRight", | ||||
| 	"bounceOutUp", | ||||
| 	// Fading exits | ||||
| 	"fadeOut", | ||||
| 	"fadeOutDown", | ||||
| 	"fadeOutDownBig", | ||||
| 	"fadeOutLeft", | ||||
| 	"fadeOutLeftBig", | ||||
| 	"fadeOutRight", | ||||
| 	"fadeOutRightBig", | ||||
| 	"fadeOutUp", | ||||
| 	"fadeOutUpBig", | ||||
| 	"fadeOutTopLeft", | ||||
| 	"fadeOutTopRight", | ||||
| 	"fadeOutBottomRight", | ||||
| 	"fadeOutBottomLeft", | ||||
| 	// Flippers | ||||
| 	"flipOutX", | ||||
| 	"flipOutY", | ||||
| 	// Lightspeed | ||||
| 	"lightSpeedOutRight", | ||||
| 	"lightSpeedOutLeft", | ||||
| 	// Rotating exits | ||||
| 	"rotateOut", | ||||
| 	"rotateOutDownLeft", | ||||
| 	"rotateOutDownRight", | ||||
| 	"rotateOutUpLeft", | ||||
| 	"rotateOutUpRight", | ||||
| 	// Specials | ||||
| 	"hinge", | ||||
| 	"rollOut", | ||||
| 	// Zooming exits | ||||
| 	"zoomOut", | ||||
| 	"zoomOutDown", | ||||
| 	"zoomOutLeft", | ||||
| 	"zoomOutRight", | ||||
| 	"zoomOutUp", | ||||
| 	// Sliding exits | ||||
| 	"slideOutDown", | ||||
| 	"slideOutLeft", | ||||
| 	"slideOutRight", | ||||
| 	"slideOutUp" | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|  * Create an animation with Animate CSS | ||||
|  * @param {string} [element] div element to animate. | ||||
|  * @param {string} [animation] animation name. | ||||
|  * @param {number} [animationTime] animation duration. | ||||
|  */ | ||||
| function addAnimateCSS (element, animation, animationTime) { | ||||
| 	const animationName = `animate__${animation}`; | ||||
| 	const node = document.getElementById(element); | ||||
| 	if (!node) { | ||||
| 		// don't execute animate: we don't find div | ||||
| 		Log.warn("addAnimateCSS: node not found for", element); | ||||
| 		return; | ||||
| 	} | ||||
| 	node.style.setProperty("--animate-duration", `${animationTime}s`); | ||||
| 	node.classList.add("animate__animated", animationName); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Remove an animation with Animate CSS | ||||
|  * @param {string} [element] div element to animate. | ||||
|  * @param {string} [animation] animation name. | ||||
|  */ | ||||
| function removeAnimateCSS (element, animation) { | ||||
| 	const animationName = `animate__${animation}`; | ||||
| 	const node = document.getElementById(element); | ||||
| 	if (!node) { | ||||
| 		// don't execute animate: we don't find div | ||||
| 		Log.warn("removeAnimateCSS: node not found for", element); | ||||
| 		return; | ||||
| 	} | ||||
| 	node.classList.remove("animate__animated", animationName); | ||||
| 	node.style.removeProperty("--animate-duration"); | ||||
| } | ||||
| if (typeof window === "undefined") module.exports = { AnimateCSSIn, AnimateCSSOut }; | ||||
							
								
								
									
										333
									
								
								js/app.js
									
									
									
									
									
								
							
							
						
						
									
										333
									
								
								js/app.js
									
									
									
									
									
								
							| @@ -1,33 +1,38 @@ | ||||
| /* Magic Mirror | ||||
|  * The Core App (Server) | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
|  | ||||
| // Alias modules mentioned in package.js under _moduleAliases. | ||||
| require("module-alias/register"); | ||||
|  | ||||
| const fs = require("fs"); | ||||
| const path = require("path"); | ||||
| const fs = require("node:fs"); | ||||
| const path = require("node:path"); | ||||
| const envsub = require("envsub"); | ||||
| const Log = require("logger"); | ||||
|  | ||||
| const Server = require(`${__dirname}/server`); | ||||
| const Utils = require(`${__dirname}/utils`); | ||||
| const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`); | ||||
| const { getEnvVarsAsObj } = require(`${__dirname}/server_functions`); | ||||
|  | ||||
| // used to control fetch timeout for node_helpers | ||||
| const { setGlobalDispatcher, Agent } = require("undici"); | ||||
| // common timeout value, provide environment override in case | ||||
| const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000; | ||||
|  | ||||
| // Get version number. | ||||
| global.version = require(`${__dirname}/../package.json`).version; | ||||
| Log.log("Starting MagicMirror: v" + global.version); | ||||
| global.mmTestMode = process.env.mmTestMode === "true"; | ||||
| Log.log(`Starting MagicMirror: v${global.version}`); | ||||
|  | ||||
| // Log system information. | ||||
| Utils.logSystemInformation(); | ||||
|  | ||||
| // global absolute root path | ||||
| global.root_path = path.resolve(`${__dirname}/../`); | ||||
|  | ||||
| if (process.env.MM_CONFIG_FILE) { | ||||
| 	global.configuration_file = process.env.MM_CONFIG_FILE; | ||||
| 	global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, ""); | ||||
| } | ||||
|  | ||||
| // FIXME: Hotfix Pull Request | ||||
| // https://github.com/MichMich/MagicMirror/pull/673 | ||||
| // https://github.com/MagicMirrorOrg/MagicMirror/pull/673 | ||||
| if (process.env.MM_PORT) { | ||||
| 	global.mmPort = process.env.MM_PORT; | ||||
| } | ||||
| @@ -35,99 +40,192 @@ if (process.env.MM_PORT) { | ||||
| // The next part is here to prevent a major exception when there | ||||
| // is no internet connection. This could probable be solved better. | ||||
| process.on("uncaughtException", function (err) { | ||||
| 	Log.error("Whoops! There was an uncaught exception..."); | ||||
| 	Log.error(err); | ||||
| 	Log.error("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?"); | ||||
| 	Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues"); | ||||
| 	// ignore strange exceptions under aarch64 coming from systeminformation: | ||||
| 	if (!err.stack.includes("node_modules/systeminformation")) { | ||||
| 		Log.error("Whoops! There was an uncaught exception..."); | ||||
| 		Log.error(err); | ||||
| 		Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?"); | ||||
| 		Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MagicMirrorOrg/MagicMirror/issues"); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * The core app. | ||||
|  * | ||||
|  * @class | ||||
|  */ | ||||
| function App() { | ||||
| function App () { | ||||
| 	let nodeHelpers = []; | ||||
| 	let httpServer; | ||||
|  | ||||
| 	/** | ||||
| 	 * Loads the config file. Combines it with the defaults, and runs the | ||||
| 	 * callback with the found config as argument. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function to be called after loading the config | ||||
| 	 * Loads the config file. Combines it with the defaults and returns the config | ||||
| 	 * @async | ||||
| 	 * @returns {Promise<object>} the loaded config or the defaults if something goes wrong | ||||
| 	 */ | ||||
| 	function loadConfig(callback) { | ||||
| 	async function loadConfig () { | ||||
| 		Log.log("Loading config ..."); | ||||
| 		const defaults = require(`${__dirname}/defaults`); | ||||
| 		if (process.env.JEST_WORKER_ID !== undefined) { | ||||
| 			// if we are running with jest | ||||
| 			defaults.address = "0.0.0.0"; | ||||
| 		} | ||||
|  | ||||
| 		// For this check proposed to TestSuite | ||||
| 		// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8 | ||||
| 		const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`); | ||||
| 		let templateFile = `${configFilename}.template`; | ||||
|  | ||||
| 		// check if templateFile exists | ||||
| 		try { | ||||
| 			fs.accessSync(templateFile, fs.constants.F_OK); | ||||
| 		} catch (err) { | ||||
| 			templateFile = null; | ||||
| 			Log.log("config template file not exists, no envsubst"); | ||||
| 		} | ||||
|  | ||||
| 		if (templateFile) { | ||||
| 			// save current config.js | ||||
| 			try { | ||||
| 				if (fs.existsSync(configFilename)) { | ||||
| 					fs.copyFileSync(configFilename, `${configFilename}-old`); | ||||
| 				} | ||||
| 			} catch (err) { | ||||
| 				Log.warn(`Could not copy ${configFilename}: ${err.message}`); | ||||
| 			} | ||||
|  | ||||
| 			// check if config.env exists | ||||
| 			const envFiles = []; | ||||
| 			const configEnvFile = `${configFilename.substr(0, configFilename.lastIndexOf("."))}.env`; | ||||
| 			try { | ||||
| 				if (fs.existsSync(configEnvFile)) { | ||||
| 					envFiles.push(configEnvFile); | ||||
| 				} | ||||
| 			} catch (err) { | ||||
| 				Log.log(`${configEnvFile} does not exist. ${err.message}`); | ||||
| 			} | ||||
|  | ||||
| 			let options = { | ||||
| 				all: true, | ||||
| 				diff: false, | ||||
| 				envFiles: envFiles, | ||||
| 				protect: false, | ||||
| 				syntax: "default", | ||||
| 				system: true | ||||
| 			}; | ||||
|  | ||||
| 			// envsubst variables in templateFile and create new config.js | ||||
| 			// naming for envsub must be templateFile and outputFile | ||||
| 			const outputFile = configFilename; | ||||
| 			try { | ||||
| 				await envsub({ templateFile, outputFile, options }); | ||||
| 			} catch (err) { | ||||
| 				Log.error(`Could not envsubst variables: ${err.message}`); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		require(`${global.root_path}/js/check_config.js`); | ||||
|  | ||||
| 		try { | ||||
| 			fs.accessSync(configFilename, fs.F_OK); | ||||
| 			fs.accessSync(configFilename, fs.constants.F_OK); | ||||
| 			const c = require(configFilename); | ||||
| 			if (Object.keys(c).length === 0) { | ||||
| 				Log.error("WARNING! Config file appears empty, maybe missing module.exports last line?"); | ||||
| 			} | ||||
| 			checkDeprecatedOptions(c); | ||||
| 			const config = Object.assign(defaults, c); | ||||
| 			callback(config); | ||||
| 			return Object.assign(defaults, c); | ||||
| 		} catch (e) { | ||||
| 			if (e.code === "ENOENT") { | ||||
| 				Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration.")); | ||||
| 				Log.error("WARNING! Could not find config file. Please create one. Starting with default configuration."); | ||||
| 			} else if (e instanceof ReferenceError || e instanceof SyntaxError) { | ||||
| 				Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`)); | ||||
| 				Log.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`); | ||||
| 			} else { | ||||
| 				Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`)); | ||||
| 				Log.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`); | ||||
| 			} | ||||
| 			callback(defaults); | ||||
| 		} | ||||
|  | ||||
| 		return defaults; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks the config for deprecated options and throws a warning in the logs | ||||
| 	 * if it encounters one option from the deprecated.js list | ||||
| 	 * | ||||
| 	 * @param {object} userConfig The user config | ||||
| 	 */ | ||||
| 	function checkDeprecatedOptions(userConfig) { | ||||
| 	function checkDeprecatedOptions (userConfig) { | ||||
| 		const deprecated = require(`${global.root_path}/js/deprecated`); | ||||
| 		const deprecatedOptions = deprecated.configs; | ||||
|  | ||||
| 		// check for deprecated core options | ||||
| 		const deprecatedOptions = deprecated.configs; | ||||
| 		const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option)); | ||||
| 		if (usedDeprecated.length > 0) { | ||||
| 			Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`)); | ||||
| 			Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`); | ||||
| 		} | ||||
|  | ||||
| 		// check for deprecated module options | ||||
| 		for (const element of userConfig.modules) { | ||||
| 			if (deprecated[element.module] !== undefined && element.config !== undefined) { | ||||
| 				const deprecatedModuleOptions = deprecated[element.module]; | ||||
| 				const usedDeprecatedModuleOptions = deprecatedModuleOptions.filter((option) => element.config.hasOwnProperty(option)); | ||||
| 				if (usedDeprecatedModuleOptions.length > 0) { | ||||
| 					Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Loads a specific module. | ||||
| 	 * | ||||
| 	 * @param {string} module The name of the module (including subpath). | ||||
| 	 * @param {Function} callback Function to be called after loading | ||||
| 	 */ | ||||
| 	function loadModule(module, callback) { | ||||
| 	function loadModule (module) { | ||||
| 		const elements = module.split("/"); | ||||
| 		const moduleName = elements[elements.length - 1]; | ||||
| 		let moduleFolder = `${__dirname}/../modules/${module}`; | ||||
| 		const env = getEnvVarsAsObj(); | ||||
| 		let moduleFolder = path.resolve(`${__dirname}/../${env.modulesDir}`, module); | ||||
|  | ||||
| 		if (defaultModules.includes(moduleName)) { | ||||
| 			moduleFolder = `${__dirname}/../modules/default/${module}`; | ||||
| 			const defaultModuleFolder = path.resolve(`${__dirname}/../modules/default/`, module); | ||||
| 			if (process.env.JEST_WORKER_ID === undefined) { | ||||
| 				moduleFolder = defaultModuleFolder; | ||||
| 			} else { | ||||
| 				// running in Jest, allow defaultModules placed under moduleDir for testing | ||||
| 				if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") { | ||||
| 					moduleFolder = defaultModuleFolder; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		const moduleFile = `${moduleFolder}/${moduleName}.js`; | ||||
|  | ||||
| 		try { | ||||
| 			fs.accessSync(moduleFile, fs.constants.R_OK); | ||||
| 		} catch (e) { | ||||
| 			Log.warn(`No ${moduleFile} found for module: ${moduleName}.`); | ||||
| 		} | ||||
|  | ||||
| 		const helperPath = `${moduleFolder}/node_helper.js`; | ||||
|  | ||||
| 		let loadHelper = true; | ||||
| 		try { | ||||
| 			fs.accessSync(helperPath, fs.R_OK); | ||||
| 			fs.accessSync(helperPath, fs.constants.R_OK); | ||||
| 		} catch (e) { | ||||
| 			loadHelper = false; | ||||
| 			Log.log(`No helper found for module: ${moduleName}.`); | ||||
| 		} | ||||
|  | ||||
| 		// if the helper was found | ||||
| 		if (loadHelper) { | ||||
| 			const Module = require(helperPath); | ||||
| 			let Module; | ||||
| 			try { | ||||
| 				Module = require(helperPath); | ||||
| 			} catch (e) { | ||||
| 				Log.error(`Error when loading ${moduleName}:`, e.message); | ||||
| 				return; | ||||
| 			} | ||||
| 			let m = new Module(); | ||||
|  | ||||
| 			if (m.requiresVersion) { | ||||
| 				Log.log(`Check MagicMirror version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`); | ||||
| 				Log.log(`Check MagicMirror² version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`); | ||||
| 				if (cmpVersions(global.version, m.requiresVersion) >= 0) { | ||||
| 					Log.log("Version is ok!"); | ||||
| 				} else { | ||||
| @@ -140,50 +238,33 @@ function App() { | ||||
| 			m.setPath(path.resolve(moduleFolder)); | ||||
| 			nodeHelpers.push(m); | ||||
|  | ||||
| 			m.loaded(callback); | ||||
| 		} else { | ||||
| 			callback(); | ||||
| 			m.loaded(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Loads all modules. | ||||
| 	 * | ||||
| 	 * @param {Module[]} modules All modules to be loaded | ||||
| 	 * @param {Function} callback Function to be called after loading | ||||
| 	 * @returns {Promise} A promise that is resolved when all modules been loaded | ||||
| 	 */ | ||||
| 	function loadModules(modules, callback) { | ||||
| 	async function loadModules (modules) { | ||||
| 		Log.log("Loading module helpers ..."); | ||||
|  | ||||
| 		/** | ||||
| 		 * | ||||
| 		 */ | ||||
| 		function loadNextModule() { | ||||
| 			if (modules.length > 0) { | ||||
| 				const nextModule = modules[0]; | ||||
| 				loadModule(nextModule, function () { | ||||
| 					modules = modules.slice(1); | ||||
| 					loadNextModule(); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				// All modules are loaded | ||||
| 				Log.log("All module helpers loaded."); | ||||
| 				callback(); | ||||
| 			} | ||||
| 		for (let module of modules) { | ||||
| 			await loadModule(module); | ||||
| 		} | ||||
|  | ||||
| 		loadNextModule(); | ||||
| 		Log.log("All module helpers loaded."); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Compare two semantic version numbers and return the difference. | ||||
| 	 * | ||||
| 	 * @param {string} a Version number a. | ||||
| 	 * @param {string} b Version number b. | ||||
| 	 * @returns {number} A positive number if a is larger than b, a negative | ||||
| 	 * number if a is smaller and 0 if they are the same | ||||
| 	 */ | ||||
| 	function cmpVersions(a, b) { | ||||
| 	function cmpVersions (a, b) { | ||||
| 		let i, diff; | ||||
| 		const regExStrip0 = /(\.0+)+$/; | ||||
| 		const segmentsA = a.replace(regExStrip0, "").split("."); | ||||
| @@ -202,43 +283,68 @@ function App() { | ||||
| 	/** | ||||
| 	 * Start the core app. | ||||
| 	 * | ||||
| 	 * It loads the config, then it loads all modules. When it's done it | ||||
| 	 * executes the callback with the config as argument. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function to be called after start | ||||
| 	 * It loads the config, then it loads all modules. | ||||
| 	 * @async | ||||
| 	 * @returns {Promise<object>} the config used | ||||
| 	 */ | ||||
| 	this.start = function (callback) { | ||||
| 		loadConfig(function (c) { | ||||
| 			config = c; | ||||
| 	this.start = async function () { | ||||
| 		config = await loadConfig(); | ||||
|  | ||||
| 			Log.setLogLevel(config.logLevel); | ||||
| 		Log.setLogLevel(config.logLevel); | ||||
|  | ||||
| 			let modules = []; | ||||
| 		// get the used module positions | ||||
| 		Utils.getModulePositions(); | ||||
|  | ||||
| 			for (const module of config.modules) { | ||||
| 				if (!modules.includes(module.module) && !module.disabled) { | ||||
| 					modules.push(module.module); | ||||
| 		let modules = []; | ||||
| 		for (const module of config.modules) { | ||||
| 			if (module.disabled) continue; | ||||
| 			if (module.module) { | ||||
| 				if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") { | ||||
| 					// Only add this module to be loaded if it is not a duplicate (repeated instance of the same module) | ||||
| 					if (!modules.includes(module.module)) { | ||||
| 						modules.push(module.module); | ||||
| 					} | ||||
| 				} else { | ||||
| 					Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); | ||||
| 				} | ||||
| 			} else { | ||||
| 				Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 			loadModules(modules, function () { | ||||
| 				const server = new Server(config, function (app, io) { | ||||
| 					Log.log("Server started ..."); | ||||
| 		setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } })); | ||||
|  | ||||
| 					for (let nodeHelper of nodeHelpers) { | ||||
| 						nodeHelper.setExpressApp(app); | ||||
| 						nodeHelper.setSocketIO(io); | ||||
| 						nodeHelper.start(); | ||||
| 					} | ||||
| 		await loadModules(modules); | ||||
|  | ||||
| 					Log.log("Sockets connected & modules started ..."); | ||||
| 		httpServer = new Server(config); | ||||
| 		const { app, io } = await httpServer.open(); | ||||
| 		Log.log("Server started ..."); | ||||
|  | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(config); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 		const nodePromises = []; | ||||
| 		for (let nodeHelper of nodeHelpers) { | ||||
| 			nodeHelper.setExpressApp(app); | ||||
| 			nodeHelper.setSocketIO(io); | ||||
|  | ||||
| 			try { | ||||
| 				nodePromises.push(nodeHelper.start()); | ||||
| 			} catch (error) { | ||||
| 				Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`); | ||||
| 				Log.error(error); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		const results = await Promise.allSettled(nodePromises); | ||||
|  | ||||
| 		// Log errors that happened during async node_helper startup | ||||
| 		results.forEach((result) => { | ||||
| 			if (result.status === "rejected") { | ||||
| 				Log.error(result.reason); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		Log.log("Sockets connected & modules started ..."); | ||||
|  | ||||
| 		return config; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| @@ -246,13 +352,40 @@ function App() { | ||||
| 	 * exists. | ||||
| 	 * | ||||
| 	 * Added to fix #1056 | ||||
| 	 * @returns {Promise} A promise that is resolved when all node_helpers and | ||||
| 	 * the http server has been closed | ||||
| 	 */ | ||||
| 	this.stop = function () { | ||||
| 		for (const nodeHelper of nodeHelpers) { | ||||
| 			if (typeof nodeHelper.stop === "function") { | ||||
| 				nodeHelper.stop(); | ||||
| 	this.stop = async function () { | ||||
| 		const nodePromises = []; | ||||
| 		for (let nodeHelper of nodeHelpers) { | ||||
| 			try { | ||||
| 				if (typeof nodeHelper.stop === "function") { | ||||
| 					nodePromises.push(nodeHelper.stop()); | ||||
| 				} | ||||
| 			} catch (error) { | ||||
| 				Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`); | ||||
| 				Log.error(error); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		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(); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| @@ -262,12 +395,12 @@ function App() { | ||||
| 	 * Note: this is only used if running `server-only`. Otherwise | ||||
| 	 * this.stop() is called by app.on("before-quit"... in `electron.js` | ||||
| 	 */ | ||||
| 	process.on("SIGINT", () => { | ||||
| 	process.on("SIGINT", async () => { | ||||
| 		Log.log("[SIGINT] Received. Shutting down server..."); | ||||
| 		setTimeout(() => { | ||||
| 			process.exit(0); | ||||
| 		}, 3000); // Force quit after 3 seconds | ||||
| 		this.stop(); | ||||
| 		await this.stop(); | ||||
| 		process.exit(0); | ||||
| 	}); | ||||
|  | ||||
| @@ -275,12 +408,12 @@ function App() { | ||||
| 	 * Listen to SIGTERM signals so we can stop everything when we | ||||
| 	 * are asked to stop by the OS. | ||||
| 	 */ | ||||
| 	process.on("SIGTERM", () => { | ||||
| 	process.on("SIGTERM", async () => { | ||||
| 		Log.log("[SIGTERM] Received. Shutting down server..."); | ||||
| 		setTimeout(() => { | ||||
| 			process.exit(0); | ||||
| 		}, 3000); // Force quit after 3 seconds | ||||
| 		this.stop(); | ||||
| 		await this.stop(); | ||||
| 		process.exit(0); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,27 +1,23 @@ | ||||
| /* Magic Mirror | ||||
|  * | ||||
|  * Check the configuration file for errors | ||||
|  * | ||||
|  * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const Linter = require("eslint").Linter; | ||||
| const linter = new Linter(); | ||||
|  | ||||
| const path = require("path"); | ||||
| const fs = require("fs"); | ||||
| const path = require("node:path"); | ||||
| const fs = require("node:fs"); | ||||
| const { styleText } = require("node:util"); | ||||
| const Ajv = require("ajv"); | ||||
| const globals = require("globals"); | ||||
| const { Linter } = require("eslint"); | ||||
|  | ||||
| const rootPath = path.resolve(`${__dirname}/../`); | ||||
| const Log = require(`${rootPath}/js/logger.js`); | ||||
| const Utils = require(`${rootPath}/js/utils.js`); | ||||
|  | ||||
| const linter = new Linter({ configType: "flat" }); | ||||
| const ajv = new Ajv(); | ||||
|  | ||||
| /** | ||||
|  * Returns a string with path of configuration file. | ||||
|  * Check if set by environment variable MM_CONFIG_FILE | ||||
|  * | ||||
|  * @returns {string} path and filename of the config file | ||||
|  */ | ||||
| function getConfigFile() { | ||||
| function getConfigFile () { | ||||
| 	// FIXME: This function should be in core. Do you want refactor me ;) ?, be good! | ||||
| 	return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`); | ||||
| } | ||||
| @@ -29,45 +25,112 @@ function getConfigFile() { | ||||
| /** | ||||
|  * Checks the config file using eslint. | ||||
|  */ | ||||
| function checkConfigFile() { | ||||
| function checkConfigFile () { | ||||
| 	const configFileName = getConfigFile(); | ||||
|  | ||||
| 	// Check if file is present | ||||
| 	if (fs.existsSync(configFileName) === false) { | ||||
| 		Log.error(Utils.colors.error("File not found: "), configFileName); | ||||
| 		throw new Error("No config file present!"); | ||||
| 		throw new Error(`File not found: ${configFileName}\nNo config file present!`); | ||||
| 	} | ||||
|  | ||||
| 	// Check permission | ||||
| 	try { | ||||
| 		fs.accessSync(configFileName, fs.F_OK); | ||||
| 	} catch (e) { | ||||
| 		Log.error(Utils.colors.error(e)); | ||||
| 		throw new Error("No permission to access config file!"); | ||||
| 		fs.accessSync(configFileName, fs.constants.F_OK); | ||||
| 	} catch (error) { | ||||
| 		throw new Error(`${error}\nNo permission to access config file!`); | ||||
| 	} | ||||
|  | ||||
| 	// Validate syntax of the configuration file. | ||||
| 	Log.info(Utils.colors.info("Checking file... "), configFileName); | ||||
| 	Log.info(`Checking config file ${configFileName} ...`); | ||||
|  | ||||
| 	// I'm not sure if all ever is utf-8 | ||||
| 	const configFile = fs.readFileSync(configFileName, "utf-8"); | ||||
|  | ||||
| 	// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}") | ||||
| 	const errors = linter.verify(configFile, { | ||||
| 		env: { | ||||
| 			es6: true | ||||
| 		} | ||||
| 	}); | ||||
| 	const errors = linter.verify( | ||||
| 		configFile, | ||||
| 		{ | ||||
| 			languageOptions: { | ||||
| 				ecmaVersion: "latest", | ||||
| 				globals: { | ||||
| 					...globals.node | ||||
| 				} | ||||
| 			}, | ||||
| 			rules: { "no-undef": "error" } | ||||
| 		}, | ||||
| 		configFileName | ||||
| 	); | ||||
|  | ||||
| 	if (errors.length === 0) { | ||||
| 		Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)")); | ||||
| 		Log.info(styleText("green", "Your configuration file doesn't contain syntax errors :)")); | ||||
| 		validateModulePositions(configFileName); | ||||
| 	} else { | ||||
| 		Log.error(Utils.colors.error("Your configuration file contains syntax errors :(")); | ||||
| 		let errorMessage = "Your configuration file contains syntax errors :("; | ||||
|  | ||||
| 		for (const error of errors) { | ||||
| 			Log.error(`Line ${error.line} column ${error.column}: ${error.message}`); | ||||
| 			errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`; | ||||
| 		} | ||||
| 		throw new Error(errorMessage); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| checkConfigFile(); | ||||
| /** | ||||
|  * | ||||
|  * @param {string} configFileName - The path and filename of the configuration file to validate. | ||||
|  */ | ||||
| function validateModulePositions (configFileName) { | ||||
| 	Log.info("Checking modules structure configuration ..."); | ||||
|  | ||||
| 	const positionList = Utils.getModulePositions(); | ||||
|  | ||||
| 	// Make Ajv schema configuration of modules config | ||||
| 	// Only scan "module" and "position" | ||||
| 	const schema = { | ||||
| 		type: "object", | ||||
| 		properties: { | ||||
| 			modules: { | ||||
| 				type: "array", | ||||
| 				items: { | ||||
| 					type: "object", | ||||
| 					properties: { | ||||
| 						module: { | ||||
| 							type: "string" | ||||
| 						}, | ||||
| 						position: { | ||||
| 							type: "string", | ||||
| 							enum: positionList | ||||
| 						} | ||||
| 					}, | ||||
| 					required: ["module"] | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	// Scan all modules | ||||
| 	const validate = ajv.compile(schema); | ||||
| 	const data = require(configFileName); | ||||
|  | ||||
| 	const valid = validate(data); | ||||
| 	if (valid) { | ||||
| 		Log.info(styleText("green", "Your modules structure configuration doesn't contain errors :)")); | ||||
| 	} else { | ||||
| 		const module = validate.errors[0].instancePath.split("/")[2]; | ||||
| 		const position = validate.errors[0].instancePath.split("/")[3]; | ||||
| 		let errorMessage = "This module configuration contains errors:"; | ||||
| 		errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`; | ||||
| 		if (position) { | ||||
| 			errorMessage += `\n${position}: ${validate.errors[0].message}`; | ||||
| 			errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`; | ||||
| 		} else { | ||||
| 			errorMessage += validate.errors[0].message; | ||||
| 		} | ||||
| 		Log.error(errorMessage); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| try { | ||||
| 	checkConfigFile(); | ||||
| } catch (error) { | ||||
| 	Log.error(error.message); | ||||
| 	process.exit(1); | ||||
| } | ||||
|   | ||||
							
								
								
									
										68
									
								
								js/class.js
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								js/class.js
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| /* global Class, xyz */ | ||||
|  | ||||
| /* Simple JavaScript Inheritance | ||||
| /* | ||||
|  * Simple JavaScript Inheritance | ||||
|  * By John Resig https://johnresig.com/ | ||||
|  * | ||||
|  * Inspired by base2 and Prototype | ||||
| @@ -8,8 +9,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,47 +21,53 @@ | ||||
|  | ||||
| 	// 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) | ||||
| 		/* | ||||
| 		 * 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]) | ||||
| 			prototype[name] | ||||
| 				= typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name]) | ||||
| 					? (function (name, fn) { | ||||
| 							return function () { | ||||
| 								var tmp = this._super; | ||||
| 						return function () { | ||||
| 							const tmp = this._super; | ||||
|  | ||||
| 								// Add a new ._super() method that is the same method | ||||
| 								// but on the super-class | ||||
| 								this._super = _super[name]; | ||||
| 							/* | ||||
| 							 * Add a new ._super() method that is the same method | ||||
| 							 * but on the super-class | ||||
| 							 */ | ||||
| 							this._super = _super[name]; | ||||
|  | ||||
| 								// The method only need to be bound temporarily, so we | ||||
| 								// remove it when we're done executing | ||||
| 								var ret = fn.apply(this, arguments); | ||||
| 								this._super = tmp; | ||||
| 							/* | ||||
| 							 * The method only need to be bound temporarily, so we | ||||
| 							 * remove it when we're done executing | ||||
| 							 */ | ||||
| 							const ret = fn.apply(this, arguments); | ||||
| 							this._super = tmp; | ||||
|  | ||||
| 								return ret; | ||||
| 							}; | ||||
| 					  })(name, prop[name]) | ||||
| 							return ret; | ||||
| 						}; | ||||
| 					}(name, prop[name])) | ||||
| 					: prop[name]; | ||||
| 		} | ||||
|  | ||||
| 		/** | ||||
| 		 * The dummy class constructor | ||||
| 		 */ | ||||
| 		function Class() { | ||||
| 		function Class () { | ||||
| 			// All construction is actually done in the init method | ||||
| 			if (!initializing && this.init) { | ||||
| 				this.init.apply(this, arguments); | ||||
| @@ -78,21 +85,24 @@ | ||||
|  | ||||
| 		return Class; | ||||
| 	}; | ||||
| })(); | ||||
| }()); | ||||
|  | ||||
| /** | ||||
|  * Define the clone method for later use. Helper Method. | ||||
|  * | ||||
|  * @param {object} obj Object to be cloned | ||||
|  * @returns {object} the cloned object | ||||
|  */ | ||||
| function cloneObject(obj) { | ||||
| function cloneObject (obj) { | ||||
| 	if (obj === null || typeof obj !== "object") { | ||||
| 		return obj; | ||||
| 	} | ||||
|  | ||||
| 	var temp = obj.constructor(); // give temp the original obj's constructor | ||||
| 	for (var key in obj) { | ||||
| 	if (obj.constructor.name === "RegExp") { | ||||
| 		return new RegExp(obj); | ||||
| 	} | ||||
|  | ||||
| 	const temp = obj.constructor(); // give temp the original obj's constructor | ||||
| 	for (const key in obj) { | ||||
| 		temp[key] = cloneObject(obj[key]); | ||||
|  | ||||
| 		if (key === "lockStrings") { | ||||
|   | ||||
| @@ -1,11 +1,5 @@ | ||||
| /* global mmPort */ | ||||
|  | ||||
| /* Magic Mirror | ||||
|  * Config Defaults | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const address = "localhost"; | ||||
| let port = 8080; | ||||
| if (typeof mmPort !== "undefined") { | ||||
| @@ -25,6 +19,15 @@ const defaults = { | ||||
| 	units: "metric", | ||||
| 	zoom: 1, | ||||
| 	customCss: "css/custom.css", | ||||
| 	foreignModulesDir: "modules", | ||||
| 	// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js, | ||||
| 	// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MagicMirrorOrg/MagicMirror/issues/2847 | ||||
| 	httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false }, | ||||
|  | ||||
| 	// properties for checking if server is alive and has same startup-timestamp, the check is per default enabled | ||||
| 	// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage. | ||||
| 	checkServerInterval: 30 * 1000, | ||||
| 	reloadAfterServerRestart: false, | ||||
|  | ||||
| 	modules: [ | ||||
| 		{ | ||||
| @@ -36,7 +39,7 @@ const defaults = { | ||||
| 			position: "upper_third", | ||||
| 			classes: "large thin", | ||||
| 			config: { | ||||
| 				text: "Magic Mirror<sup>2</sup>" | ||||
| 				text: "MagicMirror²" | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| @@ -59,7 +62,7 @@ const defaults = { | ||||
| 			position: "middle_center", | ||||
| 			classes: "xsmall", | ||||
| 			config: { | ||||
| 				text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror directory<br>" + "<pre>npm run config:check</pre>" | ||||
| 				text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror² directory<br>" + "<pre>node --run config:check</pre>" | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| @@ -67,15 +70,10 @@ const defaults = { | ||||
| 			position: "bottom_bar", | ||||
| 			classes: "xsmall dimmed", | ||||
| 			config: { | ||||
| 				text: "www.michaelteeuw.nl" | ||||
| 				text: "https://magicmirror.builders/" | ||||
| 			} | ||||
| 		} | ||||
| 	], | ||||
|  | ||||
| 	paths: { | ||||
| 		modules: "modules", | ||||
| 		vendor: "vendor" | ||||
| 	} | ||||
| 	] | ||||
| }; | ||||
|  | ||||
| /*************** DO NOT EDIT THE LINE BELOW ***************/ | ||||
|   | ||||
| @@ -1,11 +1,4 @@ | ||||
| /* Magic Mirror Deprecated Config Options List | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  * | ||||
|  * Olex S. original idea this deprecated option | ||||
|  */ | ||||
|  | ||||
| module.exports = { | ||||
| 	configs: ["kioskmode"] | ||||
| 	configs: ["kioskmode"], | ||||
| 	clock: ["secondsColor"] | ||||
| }; | ||||
|   | ||||
							
								
								
									
										169
									
								
								js/electron.js
									
									
									
									
									
								
							
							
						
						
									
										169
									
								
								js/electron.js
									
									
									
									
									
								
							| @@ -1,28 +1,54 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const electron = require("electron"); | ||||
| const core = require("./app.js"); | ||||
| const Log = require("logger"); | ||||
| const core = require("./app"); | ||||
| const Log = require("./logger"); | ||||
|  | ||||
| // Config | ||||
| let config = process.env.config ? JSON.parse(process.env.config) : {}; | ||||
| // Module to control application life. | ||||
| const app = electron.app; | ||||
|  | ||||
| /* | ||||
|  * Per default electron is started with --disable-gpu flag, if you want the gpu enabled, | ||||
|  * you must set the env var ELECTRON_ENABLE_GPU=1 on startup. | ||||
|  * See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info. | ||||
|  */ | ||||
| if (process.env.ELECTRON_ENABLE_GPU !== "1") { | ||||
| 	app.disableHardwareAcceleration(); | ||||
| } | ||||
|  | ||||
| // Module to create native browser window. | ||||
| const BrowserWindow = electron.BrowserWindow; | ||||
|  | ||||
| // Keep a global reference of the window object, if you don't, the window will | ||||
| // be closed automatically when the JavaScript object is garbage collected. | ||||
| /* | ||||
|  * Keep a global reference of the window object, if you don't, the window will | ||||
|  * be closed automatically when the JavaScript object is garbage collected. | ||||
|  */ | ||||
| let mainWindow; | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  */ | ||||
| function createWindow() { | ||||
| 	app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); | ||||
| function createWindow () { | ||||
|  | ||||
| 	/* | ||||
| 	 * see https://www.electronjs.org/docs/latest/api/screen | ||||
| 	 * Create a window that fills the screen's available work area. | ||||
| 	 */ | ||||
| 	let electronSize = (800, 600); | ||||
| 	try { | ||||
| 		electronSize = electron.screen.getPrimaryDisplay().workAreaSize; | ||||
| 	} catch { | ||||
| 		Log.warn("Could not get display size, using defaults ..."); | ||||
| 	} | ||||
|  | ||||
| 	let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"]; | ||||
| 	app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches)); | ||||
| 	let electronOptionsDefaults = { | ||||
| 		width: 800, | ||||
| 		height: 600, | ||||
| 		width: electronSize.width, | ||||
| 		height: electronSize.height, | ||||
| 		icon: "mm2.png", | ||||
| 		x: 0, | ||||
| 		y: 0, | ||||
| 		darkTheme: true, | ||||
| @@ -34,43 +60,73 @@ function createWindow() { | ||||
| 		backgroundColor: "#000000" | ||||
| 	}; | ||||
|  | ||||
| 	// DEPRECATED: "kioskmode" backwards compatibility, to be removed | ||||
| 	// settings these options directly instead provides cleaner interface | ||||
| 	/* | ||||
| 	 * DEPRECATED: "kioskmode" backwards compatibility, to be removed | ||||
| 	 * settings these options directly instead provides cleaner interface | ||||
| 	 */ | ||||
| 	if (config.kioskmode) { | ||||
| 		electronOptionsDefaults.kiosk = true; | ||||
| 	} else { | ||||
| 		electronOptionsDefaults.show = false; | ||||
| 		electronOptionsDefaults.frame = false; | ||||
| 		electronOptionsDefaults.transparent = true; | ||||
| 		electronOptionsDefaults.hasShadow = false; | ||||
| 		electronOptionsDefaults.fullscreen = true; | ||||
| 		electronOptionsDefaults.autoHideMenuBar = true; | ||||
| 	} | ||||
|  | ||||
| 	const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions); | ||||
|  | ||||
| 	if (process.env.JEST_WORKER_ID !== undefined && process.env.MOCK_DATE !== undefined) { | ||||
| 		// if we are running with jest and we want to mock the current date | ||||
| 		const fakeNow = new Date(process.env.MOCK_DATE).valueOf(); | ||||
| 		Date = class extends Date { | ||||
| 			constructor (...args) { | ||||
| 				if (args.length === 0) { | ||||
| 					super(fakeNow); | ||||
| 				} else { | ||||
| 					super(...args); | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 		const __DateNowOffset = fakeNow - Date.now(); | ||||
| 		const __DateNow = Date.now; | ||||
| 		Date.now = () => __DateNow() + __DateNowOffset; | ||||
| 	} | ||||
|  | ||||
| 	// Create the browser window. | ||||
| 	mainWindow = new BrowserWindow(electronOptions); | ||||
|  | ||||
| 	// and load the index.html of the app. | ||||
| 	// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost | ||||
| 	/* | ||||
| 	 * and load the index.html of the app. | ||||
| 	 * If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost | ||||
| 	 */ | ||||
|  | ||||
| 	let prefix; | ||||
| 	if (config["tls"] !== null && config["tls"]) { | ||||
| 	if ((config.tls !== null && config.tls) || config.useHttps) { | ||||
| 		prefix = "https://"; | ||||
| 	} else { | ||||
| 		prefix = "http://"; | ||||
| 	} | ||||
|  | ||||
| 	let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address; | ||||
| 	mainWindow.loadURL(`${prefix}${address}:${config.port}`); | ||||
| 	let address = (config.address === void 0) | (config.address === "") | (config.address === "0.0.0.0") ? (config.address = "localhost") : config.address; | ||||
| 	const port = process.env.MM_PORT || config.port; | ||||
| 	mainWindow.loadURL(`${prefix}${address}:${port}`); | ||||
|  | ||||
| 	// Open the DevTools if run with "npm start dev" | ||||
| 	// Open the DevTools if run with "node --run start:dev" | ||||
| 	if (process.argv.includes("dev")) { | ||||
| 		if (process.env.JEST_WORKER_ID !== undefined) { | ||||
| 			// if we are running with jest | ||||
| 			var devtools = new BrowserWindow(electronOptions); | ||||
| 			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; | ||||
| @@ -91,48 +147,89 @@ 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 () { | ||||
| 	// On OS X it's common to re-create a window in the app when the | ||||
| 	// dock icon is clicked and there are no other windows open. | ||||
|  | ||||
| 	/* | ||||
| 	 * On OS X it's common to re-create a window in the app when the | ||||
| 	 * dock icon is clicked and there are no other windows open. | ||||
| 	 */ | ||||
| 	if (mainWindow === null) { | ||||
| 		createWindow(); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| /* This method will be called when SIGINT is received and will call | ||||
| /* | ||||
|  * This method will be called when SIGINT is received and will call | ||||
|  * each node_helper's stop function if it exists. Added to fix #1056 | ||||
|  * | ||||
|  * Note: this is only used if running Electron. Otherwise | ||||
|  * core.stop() is called by process.on("SIGINT"... in `app.js` | ||||
|  */ | ||||
| app.on("before-quit", (event) => { | ||||
| app.on("before-quit", async (event) => { | ||||
| 	Log.log("Shutting down server..."); | ||||
| 	event.preventDefault(); | ||||
| 	setTimeout(() => { | ||||
| 		process.exit(0); | ||||
| 	}, 3000); // Force-quit after 3 seconds. | ||||
| 	core.stop(); | ||||
| 	await core.stop(); | ||||
| 	process.exit(0); | ||||
| }); | ||||
|  | ||||
| // Start the core application if server is run on localhost | ||||
| // This starts all node helpers and starts the webserver. | ||||
| if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) { | ||||
| 	core.start(function (c) { | ||||
| 		config = c; | ||||
| /** | ||||
|  * Handle errors from self-signed certificates | ||||
|  */ | ||||
| app.on("certificate-error", (event, webContents, url, error, certificate, callback) => { | ||||
| 	event.preventDefault(); | ||||
| 	callback(true); | ||||
| }); | ||||
|  | ||||
| if (process.env.clientonly) { | ||||
| 	app.whenReady().then(() => { | ||||
| 		Log.log("Launching client viewer application."); | ||||
| 		createWindow(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Start the core application if server is run on localhost | ||||
|  * This starts all node helpers and starts the webserver. | ||||
|  */ | ||||
| if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) { | ||||
| 	core.start().then((c) => { | ||||
| 		config = c; | ||||
| 		app.whenReady().then(() => { | ||||
| 			Log.log("Launching application."); | ||||
| 			createWindow(); | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
							
								
								
									
										255
									
								
								js/loader.js
									
									
									
									
									
								
							
							
						
						
									
										255
									
								
								js/loader.js
									
									
									
									
									
								
							| @@ -1,12 +1,7 @@ | ||||
| /* global defaultModules, vendor */ | ||||
|  | ||||
| /* Magic Mirror | ||||
|  * Module and File loaders. | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const Loader = (function () { | ||||
|  | ||||
| 	/* Create helper variables */ | ||||
|  | ||||
| 	const loadedModuleFiles = []; | ||||
| @@ -16,48 +11,44 @@ const Loader = (function () { | ||||
| 	/* Private Methods */ | ||||
|  | ||||
| 	/** | ||||
| 	 * Loops thru all modules and requests load for every module. | ||||
| 	 * Retrieve object of env variables. | ||||
| 	 * @returns {object} with key: values as assembled in js/server_functions.js | ||||
| 	 */ | ||||
| 	const loadModules = function () { | ||||
| 		let moduleData = getModuleData(); | ||||
|  | ||||
| 		const loadNextModule = function () { | ||||
| 			if (moduleData.length > 0) { | ||||
| 				const nextModule = moduleData[0]; | ||||
| 				loadModule(nextModule, function () { | ||||
| 					moduleData = moduleData.slice(1); | ||||
| 					loadNextModule(); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				// All modules loaded. Load custom.css | ||||
| 				// This is done after all the modules so we can | ||||
| 				// overwrite all the defined styles. | ||||
|  | ||||
| 				loadFile(config.customCss, function () { | ||||
| 					// custom.css loaded. Start all modules. | ||||
| 					startModules(); | ||||
| 				}); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		loadNextModule(); | ||||
| 	const getEnvVars = async function () { | ||||
| 		const res = await fetch(`${location.protocol}//${location.host}${config.basePath}env`); | ||||
| 		return JSON.parse(await res.text()); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Loops thru all modules and requests start for every module. | ||||
| 	 * Loops through all modules and requests start for every module. | ||||
| 	 */ | ||||
| 	const startModules = function () { | ||||
| 	const startModules = async function () { | ||||
| 		const modulePromises = []; | ||||
| 		for (const module of moduleObjects) { | ||||
| 			module.start(); | ||||
| 			try { | ||||
| 				modulePromises.push(module.start()); | ||||
| 			} catch (error) { | ||||
| 				Log.error(`Error when starting node_helper for module ${module.name}:`); | ||||
| 				Log.error(error); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		const results = await Promise.allSettled(modulePromises); | ||||
|  | ||||
| 		// Log errors that happened during async node_helper startup | ||||
| 		results.forEach((result) => { | ||||
| 			if (result.status === "rejected") { | ||||
| 				Log.error(result.reason); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		// Notify core of loaded modules. | ||||
| 		MM.modulesStarted(moduleObjects); | ||||
|  | ||||
| 		// Starting modules also hides any modules that have requested to be initially hidden | ||||
| 		for (const thisModule of moduleObjects) { | ||||
| 			if (thisModule.data.hiddenOnStartup) { | ||||
| 				Log.info("Initially hiding " + thisModule.name); | ||||
| 				Log.info(`Initially hiding ${thisModule.name}`); | ||||
| 				thisModule.hide(); | ||||
| 			} | ||||
| 		} | ||||
| @@ -65,31 +56,39 @@ const Loader = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Retrieve list of all modules. | ||||
| 	 * | ||||
| 	 * @returns {object[]} module data as configured in config | ||||
| 	 */ | ||||
| 	const getAllModules = function () { | ||||
| 		return config.modules; | ||||
| 		const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined")); | ||||
| 		return AllModules; | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Generate array with module information including module paths. | ||||
| 	 * | ||||
| 	 * @returns {object[]} Module information. | ||||
| 	 */ | ||||
| 	const getModuleData = function () { | ||||
| 	const getModuleData = async function () { | ||||
| 		const modules = getAllModules(); | ||||
| 		const moduleFiles = []; | ||||
| 		const envVars = await getEnvVars(); | ||||
|  | ||||
| 		modules.forEach(function (moduleData, index) { | ||||
| 			const module = moduleData.module; | ||||
|  | ||||
| 			const elements = module.split("/"); | ||||
| 			const moduleName = elements[elements.length - 1]; | ||||
| 			let moduleFolder = config.paths.modules + "/" + module; | ||||
| 			let moduleFolder = `${envVars.modulesDir}/${module}`; | ||||
|  | ||||
| 			if (defaultModules.indexOf(moduleName) !== -1) { | ||||
| 				moduleFolder = config.paths.modules + "/default/" + module; | ||||
| 				const defaultModuleFolder = `modules/default/${module}`; | ||||
| 				if (window.name !== "jsdom") { | ||||
| 					moduleFolder = defaultModuleFolder; | ||||
| 				} else { | ||||
| 					// running in Jest, allow defaultModules placed under moduleDir for testing | ||||
| 					if (envVars.modulesDir === "modules") { | ||||
| 						moduleFolder = defaultModuleFolder; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (moduleData.disabled === true) { | ||||
| @@ -98,16 +97,19 @@ const Loader = (function () { | ||||
|  | ||||
| 			moduleFiles.push({ | ||||
| 				index: index, | ||||
| 				identifier: "module_" + index + "_" + module, | ||||
| 				identifier: `module_${index}_${module}`, | ||||
| 				name: moduleName, | ||||
| 				path: moduleFolder + "/", | ||||
| 				file: moduleName + ".js", | ||||
| 				path: `${moduleFolder}/`, | ||||
| 				file: `${moduleName}.js`, | ||||
| 				position: moduleData.position, | ||||
| 				animateIn: moduleData.animateIn, | ||||
| 				animateOut: moduleData.animateOut, | ||||
| 				hiddenOnStartup: moduleData.hiddenOnStartup, | ||||
| 				header: moduleData.header, | ||||
| 				configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false, | ||||
| 				config: moduleData.config, | ||||
| 				classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module | ||||
| 				classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module, | ||||
| 				order: (typeof moduleData.order === "number" && Number.isInteger(moduleData.order)) ? moduleData.order : 0 | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| @@ -115,134 +117,135 @@ const Loader = (function () { | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Load modules via ajax request and create module objects.s | ||||
| 	 * | ||||
| 	 * Load modules via ajax request and create module objects. | ||||
| 	 * @param {object} module Information about the module we want to load. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} resolved when module is loaded | ||||
| 	 */ | ||||
| 	const loadModule = function (module, callback) { | ||||
| 	const loadModule = async function (module) { | ||||
| 		const url = module.path + module.file; | ||||
|  | ||||
| 		const afterLoad = function () { | ||||
| 		/** | ||||
| 		 * @returns {Promise<void>} | ||||
| 		 */ | ||||
| 		const afterLoad = async function () { | ||||
| 			const moduleObject = Module.create(module.name); | ||||
| 			if (moduleObject) { | ||||
| 				bootstrapModule(module, moduleObject, function () { | ||||
| 					callback(); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				callback(); | ||||
| 				await bootstrapModule(module, moduleObject); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		if (loadedModuleFiles.indexOf(url) !== -1) { | ||||
| 			afterLoad(); | ||||
| 			await afterLoad(); | ||||
| 		} else { | ||||
| 			loadFile(url, function () { | ||||
| 				loadedModuleFiles.push(url); | ||||
| 				afterLoad(); | ||||
| 			}); | ||||
| 			await loadFile(url); | ||||
| 			loadedModuleFiles.push(url); | ||||
| 			await afterLoad(); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Bootstrap modules by setting the module data and loading the scripts & styles. | ||||
| 	 * | ||||
| 	 * @param {object} module Information about the module we want to load. | ||||
| 	 * @param {Module} mObj Modules instance. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 */ | ||||
| 	const bootstrapModule = function (module, mObj, callback) { | ||||
| 		Log.info("Bootstrapping module: " + module.name); | ||||
|  | ||||
| 	const bootstrapModule = async function (module, mObj) { | ||||
| 		Log.info(`Bootstrapping module: ${module.name}`); | ||||
| 		mObj.setData(module); | ||||
|  | ||||
| 		mObj.loadScripts(function () { | ||||
| 			Log.log("Scripts loaded for: " + module.name); | ||||
| 			mObj.loadStyles(function () { | ||||
| 				Log.log("Styles loaded for: " + module.name); | ||||
| 				mObj.loadTranslations(function () { | ||||
| 					Log.log("Translations loaded for: " + module.name); | ||||
| 					moduleObjects.push(mObj); | ||||
| 					callback(); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 		await mObj.loadScripts(); | ||||
| 		Log.log(`Scripts loaded for: ${module.name}`); | ||||
|  | ||||
| 		await mObj.loadStyles(); | ||||
| 		Log.log(`Styles loaded for: ${module.name}`); | ||||
|  | ||||
| 		await mObj.loadTranslations(); | ||||
| 		Log.log(`Translations loaded for: ${module.name}`); | ||||
|  | ||||
| 		moduleObjects.push(mObj); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Load a script or stylesheet by adding it to the dom. | ||||
| 	 * | ||||
| 	 * @param {string} fileName Path of the file we want to load. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise} resolved when the file is loaded | ||||
| 	 */ | ||||
| 	const loadFile = function (fileName, callback) { | ||||
| 	const loadFile = async function (fileName) { | ||||
| 		const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1); | ||||
| 		let script, stylesheet; | ||||
|  | ||||
| 		switch (extension.toLowerCase()) { | ||||
| 			case "js": | ||||
| 				Log.log("Load script: " + fileName); | ||||
| 				script = document.createElement("script"); | ||||
| 				script.type = "text/javascript"; | ||||
| 				script.src = fileName; | ||||
| 				script.onload = function () { | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}; | ||||
| 				script.onerror = function () { | ||||
| 					Log.error("Error on loading script:", fileName); | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}; | ||||
|  | ||||
| 				document.getElementsByTagName("body")[0].appendChild(script); | ||||
| 				break; | ||||
| 				return new Promise((resolve) => { | ||||
| 					Log.log(`Load script: ${fileName}`); | ||||
| 					script = document.createElement("script"); | ||||
| 					script.type = "text/javascript"; | ||||
| 					script.src = fileName; | ||||
| 					script.onload = function () { | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					script.onerror = function () { | ||||
| 						Log.error("Error on loading script:", fileName); | ||||
| 						script.remove(); | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					document.getElementsByTagName("body")[0].appendChild(script); | ||||
| 				}); | ||||
| 			case "css": | ||||
| 				Log.log("Load stylesheet: " + fileName); | ||||
| 				stylesheet = document.createElement("link"); | ||||
| 				stylesheet.rel = "stylesheet"; | ||||
| 				stylesheet.type = "text/css"; | ||||
| 				stylesheet.href = fileName; | ||||
| 				stylesheet.onload = function () { | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}; | ||||
| 				stylesheet.onerror = function () { | ||||
| 					Log.error("Error on loading stylesheet:", fileName); | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}; | ||||
| 				return new Promise((resolve) => { | ||||
| 					Log.log(`Load stylesheet: ${fileName}`); | ||||
|  | ||||
| 				document.getElementsByTagName("head")[0].appendChild(stylesheet); | ||||
| 				break; | ||||
| 					stylesheet = document.createElement("link"); | ||||
| 					stylesheet.rel = "stylesheet"; | ||||
| 					stylesheet.type = "text/css"; | ||||
| 					stylesheet.href = fileName; | ||||
| 					stylesheet.onload = function () { | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					stylesheet.onerror = function () { | ||||
| 						Log.error("Error on loading stylesheet:", fileName); | ||||
| 						stylesheet.remove(); | ||||
| 						resolve(); | ||||
| 					}; | ||||
| 					document.getElementsByTagName("head")[0].appendChild(stylesheet); | ||||
| 				}); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	/* Public Methods */ | ||||
| 	return { | ||||
|  | ||||
| 		/** | ||||
| 		 * Load all modules as defined in the config. | ||||
| 		 */ | ||||
| 		loadModules: function () { | ||||
| 			loadModules(); | ||||
| 		async loadModules () { | ||||
| 			const moduleData = await getModuleData(); | ||||
| 			const envVars = await getEnvVars(); | ||||
| 			const customCss = envVars.customCss; | ||||
|  | ||||
| 			// Load all modules | ||||
| 			for (const module of moduleData) { | ||||
| 				await loadModule(module); | ||||
| 			} | ||||
|  | ||||
| 			// Load custom.css | ||||
| 			// Since this happens after loading the modules, | ||||
| 			// it overwrites the default styles. | ||||
| 			await loadFile(customCss); | ||||
|  | ||||
| 			// Start all modules. | ||||
| 			await startModules(); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Load a file (script or stylesheet). | ||||
| 		 * Prevent double loading and search for files in the vendor folder. | ||||
| 		 * | ||||
| 		 * @param {string} fileName Path of the file we want to load. | ||||
| 		 * @param {Module} module The module that calls the loadFile function. | ||||
| 		 * @param {Function} callback Function called when done. | ||||
| 		 * @returns {Promise} resolved when the file is loaded | ||||
| 		 */ | ||||
| 		loadFile: function (fileName, module, callback) { | ||||
| 		async loadFileForModule (fileName, module) { | ||||
| 			if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) { | ||||
| 				Log.log("File already loaded: " + fileName); | ||||
| 				callback(); | ||||
| 				Log.log(`File already loaded: ${fileName}`); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| @@ -250,22 +253,20 @@ const Loader = (function () { | ||||
| 				// This is an absolute or relative path. | ||||
| 				// Load it and then return. | ||||
| 				loadedFiles.push(fileName.toLowerCase()); | ||||
| 				loadFile(fileName, callback); | ||||
| 				return; | ||||
| 				return loadFile(fileName); | ||||
| 			} | ||||
|  | ||||
| 			if (vendor[fileName] !== undefined) { | ||||
| 				// This file is available in the vendor folder. | ||||
| 				// Load it from this vendor folder. | ||||
| 				loadedFiles.push(fileName.toLowerCase()); | ||||
| 				loadFile(config.paths.vendor + "/" + vendor[fileName], callback); | ||||
| 				return; | ||||
| 				return loadFile(`${vendor[fileName]}`); | ||||
| 			} | ||||
|  | ||||
| 			// File not loaded yet. | ||||
| 			// Load it based on the module path. | ||||
| 			loadedFiles.push(fileName.toLowerCase()); | ||||
| 			loadFile(module.file(fileName), callback); | ||||
| 			return loadFile(module.file(fileName)); | ||||
| 		} | ||||
| 	}; | ||||
| })(); | ||||
| }()); | ||||
|   | ||||
							
								
								
									
										135
									
								
								js/logger.js
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								js/logger.js
									
									
									
									
									
								
							| @@ -1,50 +1,109 @@ | ||||
| /* Magic Mirror | ||||
|  * Log | ||||
|  * | ||||
|  * This logger is very simple, but needs to be extended. | ||||
|  * This system can eventually be used to push the log messages to an external target. | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| // This logger is very simple, but needs to be extended. | ||||
| (function (root, factory) { | ||||
| 	if (typeof exports === "object") { | ||||
| 		// 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) { | ||||
| 			const { styleText } = require("node:util"); | ||||
|  | ||||
| 			// add timestamps in front of log messages | ||||
| 			require("console-stamp")(console, { | ||||
| 				format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :msg", | ||||
| 				tokens: { | ||||
| 					label: (arg) => { | ||||
| 						const { method, defaultTokens } = arg; | ||||
| 						let label = defaultTokens.label(arg); | ||||
| 						switch (method) { | ||||
| 							case "error": | ||||
| 								label = styleText("red", label); | ||||
| 								break; | ||||
| 							case "warn": | ||||
| 								label = styleText("yellow", label); | ||||
| 								break; | ||||
| 							case "debug": | ||||
| 								label = styleText("bgBlue", label); | ||||
| 								break; | ||||
| 							case "info": | ||||
| 								label = styleText("blue", label); | ||||
| 								break; | ||||
| 						} | ||||
| 						return label; | ||||
| 					}, | ||||
| 					msg: (arg) => { | ||||
| 						const { method, defaultTokens } = arg; | ||||
| 						let msg = defaultTokens.msg(arg); | ||||
| 						switch (method) { | ||||
| 							case "error": | ||||
| 								msg = styleText("red", msg); | ||||
| 								break; | ||||
| 							case "warn": | ||||
| 								msg = styleText("yellow", msg); | ||||
| 								break; | ||||
| 							case "info": | ||||
| 								msg = styleText("blue", msg); | ||||
| 								break; | ||||
| 						} | ||||
| 						return msg; | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 		// Node, CommonJS-like | ||||
| 		module.exports = factory(root.config); | ||||
| 	} else { | ||||
| 		// Browser globals (root is window) | ||||
| 		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) | ||||
| 	}; | ||||
| }(this, function (config) { | ||||
| 	let logLevel; | ||||
| 	let enableLog; | ||||
| 	if (typeof exports === "object") { | ||||
| 		// in nodejs and not running with jest | ||||
| 		enableLog = process.env.JEST_WORKER_ID === undefined; | ||||
| 	} else { | ||||
| 		// in browser and not running with jsdom | ||||
| 		enableLog = typeof window === "object" && window.name !== "jsdom"; | ||||
| 	} | ||||
|  | ||||
| 	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) { | ||||
| 					if (!newLevel.includes(key.toLocaleUpperCase())) { | ||||
| 						logLevel[key] = function () {}; | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		}; | ||||
| 	} else { | ||||
| 		logLevel = { | ||||
| 			debug () {}, | ||||
| 			log () {}, | ||||
| 			info () {}, | ||||
| 			warn () {}, | ||||
| 			error () {}, | ||||
| 			group () {}, | ||||
| 			groupCollapsed () {}, | ||||
| 			groupEnd () {}, | ||||
| 			time () {}, | ||||
| 			timeEnd () {}, | ||||
| 			timeStamp () {} | ||||
| 		}; | ||||
|  | ||||
| 		logLevel.setLogLevel = function () {}; | ||||
| 	} | ||||
|  | ||||
| 	return logLevel; | ||||
| }); | ||||
| })); | ||||
|   | ||||
							
								
								
									
										303
									
								
								js/main.js
									
									
									
									
									
								
							
							
						
						
									
										303
									
								
								js/main.js
									
									
									
									
									
								
							| @@ -1,11 +1,5 @@ | ||||
| /* global Loader, defaults, Translator */ | ||||
| /* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */ | ||||
|  | ||||
| /* Magic Mirror | ||||
|  * Main System | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const MM = (function () { | ||||
| 	let modules = []; | ||||
|  | ||||
| @@ -22,6 +16,10 @@ const MM = (function () { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			let haveAnimateIn = null; | ||||
| 			// check if have valid animateIn in module definition (module.data.animateIn) | ||||
| 			if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn; | ||||
|  | ||||
| 			const wrapper = selectWrapper(module.data.position); | ||||
|  | ||||
| 			const dom = document.createElement("div"); | ||||
| @@ -29,9 +27,11 @@ const MM = (function () { | ||||
| 			dom.className = module.name; | ||||
|  | ||||
| 			if (typeof module.data.classes === "string") { | ||||
| 				dom.className = "module " + dom.className + " " + module.data.classes; | ||||
| 				dom.className = `module ${dom.className} ${module.data.classes}`; | ||||
| 			} | ||||
|  | ||||
| 			dom.style.order = (typeof module.data.order === "number" && Number.isInteger(module.data.order)) ? module.data.order : 0; | ||||
|  | ||||
| 			dom.opacity = 0; | ||||
| 			wrapper.appendChild(dom); | ||||
|  | ||||
| @@ -50,7 +50,12 @@ const MM = (function () { | ||||
| 			moduleContent.className = "module-content"; | ||||
| 			dom.appendChild(moduleContent); | ||||
|  | ||||
| 			const domCreationPromise = updateDom(module, 0); | ||||
| 			// create the domCreationPromise with AnimateCSS (with animateIn of module definition) | ||||
| 			// or just display it | ||||
| 			var domCreationPromise; | ||||
| 			if (haveAnimateIn) domCreationPromise = updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true); | ||||
| 			else domCreationPromise = updateDom(module, 0); | ||||
|  | ||||
| 			domCreationPromises.push(domCreationPromise); | ||||
| 			domCreationPromise | ||||
| 				.then(function () { | ||||
| @@ -68,9 +73,8 @@ const MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Select the wrapper dom object for a specific position. | ||||
| 	 * | ||||
| 	 * @param {string} position The name of the position. | ||||
| 	 * @returns {HTMLElement} the wrapper element | ||||
| 	 * @returns {HTMLElement | void} the wrapper element | ||||
| 	 */ | ||||
| 	const selectWrapper = function (position) { | ||||
| 		const classes = position.replace("_", " "); | ||||
| @@ -85,7 +89,6 @@ const MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Send a notification to all modules. | ||||
| 	 * | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 * @param {Module} sender The module that sent the notification. | ||||
| @@ -102,13 +105,31 @@ const MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Update the dom for a specific module. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module that needs an update. | ||||
| 	 * @param {number} [speed] The (optional) number of microseconds for the animation. | ||||
| 	 * @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates) | ||||
| 	 * @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror) | ||||
| 	 * @returns {Promise} Resolved when the dom is fully updated. | ||||
| 	 */ | ||||
| 	const updateDom = function (module, speed) { | ||||
| 	const updateDom = function (module, updateOptions, createAnimatedDom = false) { | ||||
| 		return new Promise(function (resolve) { | ||||
| 			let speed = updateOptions; | ||||
| 			let animateOut = null; | ||||
| 			let animateIn = null; | ||||
| 			if (typeof updateOptions === "object") { | ||||
| 				if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) { | ||||
| 					speed = updateOptions.options.speed; | ||||
| 					Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`); | ||||
| 					if (typeof updateOptions.options.animate === "object") { | ||||
| 						animateOut = updateOptions.options.animate.out; | ||||
| 						animateIn = updateOptions.options.animate.in; | ||||
| 						Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`); | ||||
| 					} | ||||
| 				} else { | ||||
| 					Log.debug(`updateDom: ${module.identifier} Has no speed in object`); | ||||
| 					speed = 0; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			const newHeader = module.getHeader(); | ||||
| 			let newContentPromise = module.getDom(); | ||||
|  | ||||
| @@ -119,7 +140,7 @@ const MM = (function () { | ||||
|  | ||||
| 			newContentPromise | ||||
| 				.then(function (newContent) { | ||||
| 					const updatePromise = updateDomWithContent(module, speed, newHeader, newContent); | ||||
| 					const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom); | ||||
|  | ||||
| 					updatePromise.then(resolve).catch(Log.error); | ||||
| 				}) | ||||
| @@ -129,14 +150,16 @@ const MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Update the dom with the specified content | ||||
| 	 * | ||||
| 	 * @param {Module} module The module that needs an update. | ||||
| 	 * @param {number} [speed] The (optional) number of microseconds for the animation. | ||||
| 	 * @param {string} newHeader The new header that is generated. | ||||
| 	 * @param {HTMLElement} newContent The new content that is generated. | ||||
| 	 * @param {string} [animateOut] AnimateCss animation name before hidden | ||||
| 	 * @param {string} [animateIn] AnimateCss animation name on show | ||||
| 	 * @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start) | ||||
| 	 * @returns {Promise} Resolved when the module dom has been updated. | ||||
| 	 */ | ||||
| 	const updateDomWithContent = function (module, speed, newHeader, newContent) { | ||||
| 	const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) { | ||||
| 		return new Promise(function (resolve) { | ||||
| 			if (module.hidden || !speed) { | ||||
| 				updateModuleContent(module, newHeader, newContent); | ||||
| @@ -155,19 +178,33 @@ const MM = (function () { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			hideModule(module, speed / 2, function () { | ||||
| 			if (createAnimatedDom && animateIn !== null) { | ||||
| 				Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`); | ||||
| 				updateModuleContent(module, newHeader, newContent); | ||||
| 				if (!module.hidden) { | ||||
| 					showModule(module, speed / 2); | ||||
| 					showModule(module, speed, null, { animate: animateIn }); | ||||
| 				} | ||||
| 				resolve(); | ||||
| 			}); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			hideModule( | ||||
| 				module, | ||||
| 				speed / 2, | ||||
| 				function () { | ||||
| 					updateModuleContent(module, newHeader, newContent); | ||||
| 					if (!module.hidden) { | ||||
| 						showModule(module, speed / 2, null, { animate: animateIn }); | ||||
| 					} | ||||
| 					resolve(); | ||||
| 				}, | ||||
| 				{ animate: animateOut } | ||||
| 			); | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Check if the content has changed. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to check. | ||||
| 	 * @param {string} newHeader The new header that is generated. | ||||
| 	 * @param {HTMLElement} newContent The new content that is generated. | ||||
| @@ -198,7 +235,6 @@ const MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Update the content of a module on screen. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to check. | ||||
| 	 * @param {string} newHeader The new header that is generated. | ||||
| 	 * @param {HTMLElement} newContent The new content that is generated. | ||||
| @@ -224,15 +260,12 @@ const MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Hide the module. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to hide. | ||||
| 	 * @param {number} speed The speed of the hide animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the hide method. | ||||
| 	 */ | ||||
| 	const hideModule = function (module, speed, callback, options) { | ||||
| 		options = options || {}; | ||||
|  | ||||
| 	const hideModule = function (module, speed, callback, options = {}) { | ||||
| 		// set lockString if set in options. | ||||
| 		if (options.lockString) { | ||||
| 			// Log.log("Has lockstring: " + options.lockString); | ||||
| @@ -243,23 +276,65 @@ const MM = (function () { | ||||
|  | ||||
| 		const moduleWrapper = document.getElementById(module.identifier); | ||||
| 		if (moduleWrapper !== null) { | ||||
| 			moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; | ||||
| 			moduleWrapper.style.opacity = 0; | ||||
|  | ||||
| 			clearTimeout(module.showHideTimer); | ||||
| 			module.showHideTimer = setTimeout(function () { | ||||
| 				// To not take up any space, we just make the position absolute. | ||||
| 				// since it's fade out anyway, we can see it lay above or | ||||
| 				// below other modules. This works way better than adjusting | ||||
| 				// the .display property. | ||||
| 				moduleWrapper.style.position = "fixed"; | ||||
| 			// reset all animations if needed | ||||
| 			if (module.hasAnimateOut) { | ||||
| 				removeAnimateCSS(module.identifier, module.hasAnimateOut); | ||||
| 				Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`); | ||||
| 				module.hasAnimateOut = false; | ||||
| 			} | ||||
| 			if (module.hasAnimateIn) { | ||||
| 				removeAnimateCSS(module.identifier, module.hasAnimateIn); | ||||
| 				Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`); | ||||
| 				module.hasAnimateIn = false; | ||||
| 			} | ||||
| 			// haveAnimateName for verify if we are using AnimateCSS library | ||||
| 			// we check AnimateCSSOut Array for validate it | ||||
| 			// and finally return the animate name or `null` (for default MM² animation) | ||||
| 			let haveAnimateName = null; | ||||
| 			// check if have valid animateOut in module definition (module.data.animateOut) | ||||
| 			if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut; | ||||
| 			// can't be override with options.animate | ||||
| 			else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate; | ||||
|  | ||||
| 				updateWrapperStates(); | ||||
| 			if (haveAnimateName) { | ||||
| 				// with AnimateCSS | ||||
| 				Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`); | ||||
| 				module.hasAnimateOut = haveAnimateName; | ||||
| 				addAnimateCSS(module.identifier, haveAnimateName, speed / 1000); | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					removeAnimateCSS(module.identifier, haveAnimateName); | ||||
| 					Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`); | ||||
| 					// AnimateCSS is now done | ||||
| 					moduleWrapper.style.opacity = 0; | ||||
| 					moduleWrapper.classList.add("hidden"); | ||||
| 					moduleWrapper.style.position = "fixed"; | ||||
| 					module.hasAnimateOut = false; | ||||
|  | ||||
| 				if (typeof callback === "function") { | ||||
| 					callback(); | ||||
| 				} | ||||
| 			}, speed); | ||||
| 					updateWrapperStates(); | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} else { | ||||
| 				// default MM² Animate | ||||
| 				moduleWrapper.style.transition = `opacity ${speed / 1000}s`; | ||||
| 				moduleWrapper.style.opacity = 0; | ||||
| 				moduleWrapper.classList.add("hidden"); | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					// To not take up any space, we just make the position absolute. | ||||
| 					// since it's fade out anyway, we can see it lay above or | ||||
| 					// below other modules. This works way better than adjusting | ||||
| 					// the .display property. | ||||
| 					moduleWrapper.style.position = "fixed"; | ||||
|  | ||||
| 					updateWrapperStates(); | ||||
|  | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} | ||||
| 		} else { | ||||
| 			// invoke callback even if no content, issue 1308 | ||||
| 			if (typeof callback === "function") { | ||||
| @@ -270,15 +345,12 @@ const MM = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Show the module. | ||||
| 	 * | ||||
| 	 * @param {Module} module The module to show. | ||||
| 	 * @param {number} speed The speed of the show animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the show method. | ||||
| 	 */ | ||||
| 	const showModule = function (module, speed, callback, options) { | ||||
| 		options = options || {}; | ||||
|  | ||||
| 	const showModule = function (module, speed, callback, options = {}) { | ||||
| 		// remove lockString if set in options. | ||||
| 		if (options.lockString) { | ||||
| 			const index = module.lockStrings.indexOf(options.lockString); | ||||
| @@ -287,29 +359,52 @@ const MM = (function () { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Check if there are no more lockstrings set, or the force option is set. | ||||
| 		// Check if there are no more lockStrings set, or the force option is set. | ||||
| 		// Otherwise cancel show action. | ||||
| 		if (module.lockStrings.length !== 0 && options.force !== true) { | ||||
| 			Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(",")); | ||||
| 			Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`); | ||||
| 			if (typeof options.onError === "function") { | ||||
| 				options.onError(new Error("LOCK_STRING_ACTIVE")); | ||||
| 			} | ||||
| 			return; | ||||
| 		} | ||||
| 		// reset all animations if needed | ||||
| 		if (module.hasAnimateOut) { | ||||
| 			removeAnimateCSS(module.identifier, module.hasAnimateOut); | ||||
| 			Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`); | ||||
| 			module.hasAnimateOut = false; | ||||
| 		} | ||||
| 		if (module.hasAnimateIn) { | ||||
| 			removeAnimateCSS(module.identifier, module.hasAnimateIn); | ||||
| 			Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`); | ||||
| 			module.hasAnimateIn = false; | ||||
| 		} | ||||
|  | ||||
| 		module.hidden = false; | ||||
|  | ||||
| 		// If forced show, clean current lockstrings. | ||||
| 		// If forced show, clean current lockStrings. | ||||
| 		if (module.lockStrings.length !== 0 && options.force === true) { | ||||
| 			Log.log("Force show of module: " + module.name); | ||||
| 			Log.log(`Force show of module: ${module.name}`); | ||||
| 			module.lockStrings = []; | ||||
| 		} | ||||
|  | ||||
| 		const moduleWrapper = document.getElementById(module.identifier); | ||||
| 		if (moduleWrapper !== null) { | ||||
| 			moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; | ||||
| 			clearTimeout(module.showHideTimer); | ||||
|  | ||||
| 			// haveAnimateName for verify if we are using AnimateCSS library | ||||
| 			// we check AnimateCSSIn Array for validate it | ||||
| 			// and finally return the animate name or `null` (for default MM² animation) | ||||
| 			let haveAnimateName = null; | ||||
| 			// check if have valid animateOut in module definition (module.data.animateIn) | ||||
| 			if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn; | ||||
| 			// can't be override with options.animate | ||||
| 			else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate; | ||||
|  | ||||
| 			if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`; | ||||
| 			// Restore the position. See hideModule() for more info. | ||||
| 			moduleWrapper.style.position = "static"; | ||||
| 			moduleWrapper.classList.remove("hidden"); | ||||
|  | ||||
| 			updateWrapperStates(); | ||||
|  | ||||
| @@ -317,12 +412,27 @@ const MM = (function () { | ||||
| 			const dummy = moduleWrapper.parentElement.parentElement.offsetHeight; | ||||
| 			moduleWrapper.style.opacity = 1; | ||||
|  | ||||
| 			clearTimeout(module.showHideTimer); | ||||
| 			module.showHideTimer = setTimeout(function () { | ||||
| 				if (typeof callback === "function") { | ||||
| 					callback(); | ||||
| 				} | ||||
| 			}, speed); | ||||
| 			if (haveAnimateName) { | ||||
| 				// with AnimateCSS | ||||
| 				Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`); | ||||
| 				module.hasAnimateIn = haveAnimateName; | ||||
| 				addAnimateCSS(module.identifier, haveAnimateName, speed / 1000); | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					removeAnimateCSS(module.identifier, haveAnimateName); | ||||
| 					Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`); | ||||
| 					module.hasAnimateIn = false; | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} else { | ||||
| 				// default MM² Animate | ||||
| 				module.showHideTimer = setTimeout(function () { | ||||
| 					if (typeof callback === "function") { | ||||
| 						callback(); | ||||
| 					} | ||||
| 				}, speed); | ||||
| 			} | ||||
| 		} else { | ||||
| 			// invoke callback | ||||
| 			if (typeof callback === "function") { | ||||
| @@ -342,10 +452,9 @@ const MM = (function () { | ||||
| 	 * an ugly top margin. By using this function, the top bar will be hidden if the | ||||
| 	 * update notification is not visible. | ||||
| 	 */ | ||||
| 	const updateWrapperStates = function () { | ||||
| 		const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"]; | ||||
|  | ||||
| 		positions.forEach(function (position) { | ||||
| 	const updateWrapperStates = function () { | ||||
| 		modulePositions.forEach(function (position) { | ||||
| 			const wrapper = selectWrapper(position); | ||||
| 			const moduleWrappers = wrapper.getElementsByClassName("module"); | ||||
|  | ||||
| @@ -356,7 +465,8 @@ const MM = (function () { | ||||
| 				} | ||||
| 			}); | ||||
|  | ||||
| 			wrapper.style.display = showWrapper ? "block" : "none"; | ||||
| 			// move container definitions to main CSS | ||||
| 			wrapper.className = showWrapper ? "container" : "container hidden"; | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
| @@ -365,7 +475,6 @@ const MM = (function () { | ||||
| 	 */ | ||||
| 	const loadConfig = function () { | ||||
| 		// FIXME: Think about how to pass config around without breaking tests | ||||
| 		/* eslint-disable */ | ||||
| 		if (typeof config === "undefined") { | ||||
| 			config = defaults; | ||||
| 			Log.error("Config file is missing! Please create a config file."); | ||||
| @@ -373,18 +482,16 @@ const MM = (function () { | ||||
| 		} | ||||
|  | ||||
| 		config = Object.assign({}, defaults, config); | ||||
| 		/* eslint-enable */ | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Adds special selectors on a collection of modules. | ||||
| 	 * | ||||
| 	 * @param {Module[]} modules Array of modules. | ||||
| 	 */ | ||||
| 	const setSelectionMethodsForModules = function (modules) { | ||||
|  | ||||
| 		/** | ||||
| 		 * Filter modules with the specified classes. | ||||
| 		 * | ||||
| 		 * @param {string|string[]} className one or multiple classnames (array or space divided). | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| 		 */ | ||||
| @@ -394,7 +501,6 @@ const MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Filter modules without the specified classes. | ||||
| 		 * | ||||
| 		 * @param {string|string[]} className one or multiple classnames (array or space divided). | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| 		 */ | ||||
| @@ -404,7 +510,6 @@ const MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Filters a collection of modules based on classname(s). | ||||
| 		 * | ||||
| 		 * @param {string|string[]} className one or multiple classnames (array or space divided). | ||||
| 		 * @param {boolean} include if the filter should include or exclude the modules with the specific classes. | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| @@ -433,7 +538,6 @@ const MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Removes a module instance from the collection. | ||||
| 		 * | ||||
| 		 * @param {object} module The module instance to remove from the collection. | ||||
| 		 * @returns {Module[]} Filtered collection of modules. | ||||
| 		 */ | ||||
| @@ -448,7 +552,6 @@ const MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Walks thru a collection of modules and executes the callback with the module as an argument. | ||||
| 		 * | ||||
| 		 * @param {Function} callback The function to execute with the module as an argument. | ||||
| 		 */ | ||||
| 		const enumerate = function (callback) { | ||||
| @@ -472,44 +575,64 @@ const MM = (function () { | ||||
| 	}; | ||||
|  | ||||
| 	return { | ||||
|  | ||||
| 		/* Public Methods */ | ||||
|  | ||||
| 		/** | ||||
| 		 * Main init method. | ||||
| 		 */ | ||||
| 		init: function () { | ||||
| 			Log.info("Initializing MagicMirror."); | ||||
| 		async init () { | ||||
| 			Log.info("Initializing MagicMirror²."); | ||||
| 			loadConfig(); | ||||
|  | ||||
| 			Log.setLogLevel(config.logLevel); | ||||
|  | ||||
| 			Translator.loadCoreTranslations(config.language); | ||||
| 			Loader.loadModules(); | ||||
| 			await Translator.loadCoreTranslations(config.language); | ||||
| 			await Loader.loadModules(); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets called when all modules are started. | ||||
| 		 * | ||||
| 		 * @param {Module[]} moduleObjects All module instances. | ||||
| 		 */ | ||||
| 		modulesStarted: function (moduleObjects) { | ||||
| 		modulesStarted (moduleObjects) { | ||||
| 			modules = []; | ||||
| 			let startUp = ""; | ||||
|  | ||||
| 			moduleObjects.forEach((module) => modules.push(module)); | ||||
|  | ||||
| 			Log.info("All modules started!"); | ||||
| 			sendNotification("ALL_MODULES_STARTED"); | ||||
|  | ||||
| 			createDomObjects(); | ||||
|  | ||||
| 			if (config.reloadAfterServerRestart) { | ||||
| 				setInterval(async () => { | ||||
| 					// if server startup time has changed (which means server was restarted) | ||||
| 					// the client reloads the mm page | ||||
| 					try { | ||||
| 						const res = await fetch(`${location.protocol}//${location.host}${config.basePath}startup`); | ||||
| 						const curr = await res.text(); | ||||
| 						if (startUp === "") startUp = curr; | ||||
| 						if (startUp !== curr) { | ||||
| 							startUp = ""; | ||||
| 							window.location.reload(true); | ||||
| 							Log.warn("Refreshing Website because server was restarted"); | ||||
| 						} | ||||
| 					} catch (err) { | ||||
| 						Log.error(`MagicMirror not reachable: ${err}`); | ||||
| 					} | ||||
| 				}, config.checkServerInterval); | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Send a notification to all modules. | ||||
| 		 * | ||||
| 		 * @param {string} notification The identifier of the notification. | ||||
| 		 * @param {*} payload The payload of the notification. | ||||
| 		 * @param {Module} sender The module that sent the notification. | ||||
| 		 */ | ||||
| 		sendNotification: function (notification, payload, sender) { | ||||
| 		sendNotification (notification, payload, sender) { | ||||
| 			if (arguments.length < 3) { | ||||
| 				Log.error("sendNotification: Missing arguments."); | ||||
| 				return; | ||||
| @@ -531,11 +654,10 @@ const MM = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Update the dom for a specific module. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module that needs an update. | ||||
| 		 * @param {number} [speed] The number of microseconds for the animation. | ||||
| 		 * @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates) | ||||
| 		 */ | ||||
| 		updateDom: function (module, speed) { | ||||
| 		updateDom (module, updateOptions) { | ||||
| 			if (!(module instanceof Module)) { | ||||
| 				Log.error("updateDom: Sender should be a module."); | ||||
| 				return; | ||||
| @@ -547,46 +669,49 @@ const MM = (function () { | ||||
| 			} | ||||
|  | ||||
| 			// Further implementation is done in the private method. | ||||
| 			updateDom(module, speed); | ||||
| 			updateDom(module, updateOptions).then(function () { | ||||
| 				// Once the update is complete and rendered, send a notification to the module that the DOM has been updated | ||||
| 				sendNotification("MODULE_DOM_UPDATED", null, null, module); | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Returns a collection of all modules currently active. | ||||
| 		 * | ||||
| 		 * @returns {Module[]} A collection of all modules currently active. | ||||
| 		 */ | ||||
| 		getModules: function () { | ||||
| 		getModules () { | ||||
| 			setSelectionMethodsForModules(modules); | ||||
| 			return modules; | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Hide the module. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module to hide. | ||||
| 		 * @param {number} speed The speed of the hide animation. | ||||
| 		 * @param {Function} callback Called when the animation is done. | ||||
| 		 * @param {object} [options] Optional settings for the hide method. | ||||
| 		 */ | ||||
| 		hideModule: function (module, speed, callback, options) { | ||||
| 		hideModule (module, speed, callback, options) { | ||||
| 			module.hidden = true; | ||||
| 			hideModule(module, speed, callback, options); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Show the module. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module to show. | ||||
| 		 * @param {number} speed The speed of the show animation. | ||||
| 		 * @param {Function} callback Called when the animation is done. | ||||
| 		 * @param {object} [options] Optional settings for the show method. | ||||
| 		 */ | ||||
| 		showModule: function (module, speed, callback, options) { | ||||
| 		showModule (module, speed, callback, options) { | ||||
| 			// do not change module.hidden yet, only if we really show it later | ||||
| 			showModule(module, speed, callback, options); | ||||
| 		} | ||||
| 		}, | ||||
|  | ||||
| 		// Return all available module positions. | ||||
| 		getAvailableModulePositions: modulePositions | ||||
| 	}; | ||||
| })(); | ||||
| }()); | ||||
|  | ||||
| // Add polyfill for Object.assign. | ||||
| if (typeof Object.assign !== "function") { | ||||
| @@ -609,7 +734,7 @@ if (typeof Object.assign !== "function") { | ||||
| 			} | ||||
| 			return output; | ||||
| 		}; | ||||
| 	})(); | ||||
| 	}()); | ||||
| } | ||||
|  | ||||
| MM.init(); | ||||
|   | ||||
							
								
								
									
										239
									
								
								js/module.js
									
									
									
									
									
								
							
							
						
						
									
										239
									
								
								js/module.js
									
									
									
									
									
								
							| @@ -1,18 +1,18 @@ | ||||
| /* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */ | ||||
|  | ||||
| /* Magic Mirror | ||||
| /* | ||||
|  * Module Blueprint. | ||||
|  * @typedef {Object} Module | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var Module = Class.extend({ | ||||
| 	/********************************************************* | ||||
| 	 * All methods (and properties) below can be subclassed. * | ||||
| 	 *********************************************************/ | ||||
| const Module = Class.extend({ | ||||
|  | ||||
| 	// Set the minimum MagicMirror module version for this module. | ||||
| 	/** | ||||
| 	 ******************************************************** | ||||
| 	 * All methods (and properties) below can be subclassed. * | ||||
| 	 ******************************************************** | ||||
| 	 */ | ||||
|  | ||||
| 	// Set the minimum MagicMirror² module version for this module. | ||||
| 	requiresVersion: "2.0.0", | ||||
|  | ||||
| 	// Module config defaults. | ||||
| @@ -21,44 +21,46 @@ var Module = Class.extend({ | ||||
| 	// Timer reference used for showHide animation callbacks. | ||||
| 	showHideTimer: null, | ||||
|  | ||||
| 	// Array to store lockStrings. These strings are used to lock | ||||
| 	// visibility when hiding and showing module. | ||||
| 	/* | ||||
| 	 * Array to store lockStrings. These strings are used to lock | ||||
| 	 * visibility when hiding and showing module. | ||||
| 	 */ | ||||
| 	lockStrings: [], | ||||
|  | ||||
| 	// Storage of the nunjuck Environment, | ||||
| 	// This should not be referenced directly. | ||||
| 	// Use the nunjucksEnvironment() to get it. | ||||
| 	/* | ||||
| 	 * Storage of the nunjucks Environment, | ||||
| 	 * This should not be referenced directly. | ||||
| 	 * Use the nunjucksEnvironment() to get it. | ||||
| 	 */ | ||||
| 	_nunjucksEnvironment: null, | ||||
|  | ||||
| 	/** | ||||
| 	 * Called when the module is instantiated. | ||||
| 	 */ | ||||
| 	init: function () { | ||||
| 	init () { | ||||
| 		//Log.log(this.defaults); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Called when the module is started. | ||||
| 	 */ | ||||
| 	start: function () { | ||||
| 		Log.info("Starting module: " + this.name); | ||||
| 	async start () { | ||||
| 		Log.info(`Starting module: ${this.name}`); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns a list of scripts the module requires to be loaded. | ||||
| 	 * | ||||
| 	 * @returns {string[]} An array with filenames. | ||||
| 	 */ | ||||
| 	getScripts: function () { | ||||
| 	getScripts () { | ||||
| 		return []; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns a list of stylesheets the module requires to be loaded. | ||||
| 	 * | ||||
| 	 * @returns {string[]} An array with filenames. | ||||
| 	 */ | ||||
| 	getStyles: function () { | ||||
| 	getStyles () { | ||||
| 		return []; | ||||
| 	}, | ||||
|  | ||||
| @@ -66,28 +68,26 @@ 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 () { | ||||
| 	getTranslations () { | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Generates the dom which needs to be displayed. This method is called by the Magic Mirror core. | ||||
| 	 * Generates the dom which needs to be displayed. This method is called by the MagicMirror² core. | ||||
| 	 * This method can to be subclassed if the module wants to display info on the mirror. | ||||
| 	 * Alternatively, the getTemplate method could be subclassed. | ||||
| 	 * | ||||
| 	 * @returns {HTMLElement|Promise} The dom or a promise with the dom to display. | ||||
| 	 */ | ||||
| 	getDom: function () { | ||||
| 	getDom () { | ||||
| 		return new Promise((resolve) => { | ||||
| 			const div = document.createElement("div"); | ||||
| 			const template = this.getTemplate(); | ||||
| 			const templateData = this.getTemplateData(); | ||||
|  | ||||
| 			// Check to see if we need to render a template string or a file. | ||||
| 			if (/^.*((\.html)|(\.njk))$/.test(template)) { | ||||
| 			if ((/^.*((\.html)|(\.njk))$/).test(template)) { | ||||
| 				// the template is a filename | ||||
| 				this.nunjucksEnvironment().render(template, templateData, function (err, res) { | ||||
| 					if (err) { | ||||
| @@ -109,12 +109,11 @@ 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 () { | ||||
| 	getHeader () { | ||||
| 		return this.data.header; | ||||
| 	}, | ||||
|  | ||||
| @@ -123,31 +122,28 @@ 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>"; | ||||
| 	getTemplate () { | ||||
| 		return `<div class="normal">${this.name}</div><div class="small dimmed">${this.identifier}</div>`; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns the data to be used in the template. | ||||
| 	 * This method needs to be subclassed if the module wants to use a custom data. | ||||
| 	 * | ||||
| 	 * @returns {object} The data for the template | ||||
| 	 */ | ||||
| 	getTemplateData: function () { | ||||
| 	getTemplateData () { | ||||
| 		return {}; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Called by the Magic Mirror core when a notification arrives. | ||||
| 	 * | ||||
| 	 * Called by the MagicMirror² core when a notification arrives. | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 * @param {Module} sender The module that sent the notification. | ||||
| 	 */ | ||||
| 	notificationReceived: function (notification, payload, sender) { | ||||
| 	notificationReceived (notification, payload, sender) { | ||||
| 		if (sender) { | ||||
| 			// Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name); | ||||
| 		} else { | ||||
| @@ -158,10 +154,9 @@ 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 () { | ||||
| 	nunjucksEnvironment () { | ||||
| 		if (this._nunjucksEnvironment !== null) { | ||||
| 			return this._nunjucksEnvironment; | ||||
| 		} | ||||
| @@ -180,63 +175,63 @@ 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); | ||||
| 	socketNotificationReceived (notification, payload) { | ||||
| 		Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Called when the module is hidden. | ||||
| 	 */ | ||||
| 	suspend: function () { | ||||
| 		Log.log(this.name + " is suspended."); | ||||
| 	suspend () { | ||||
| 		Log.log(`${this.name} is suspended.`); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Called when the module is shown. | ||||
| 	 */ | ||||
| 	resume: function () { | ||||
| 		Log.log(this.name + " is resumed."); | ||||
| 	resume () { | ||||
| 		Log.log(`${this.name} is resumed.`); | ||||
| 	}, | ||||
|  | ||||
| 	/********************************************* | ||||
| 	 * The methods below don"t need subclassing. * | ||||
| 	 *********************************************/ | ||||
| 	/** | ||||
| 	 ******************************************** | ||||
| 	 * The methods below don't need subclassing. * | ||||
| 	 ******************************************** | ||||
| 	 */ | ||||
|  | ||||
| 	/** | ||||
| 	 * Set the module data. | ||||
| 	 * | ||||
| 	 * @param {object} data The module data | ||||
| 	 */ | ||||
| 	setData: function (data) { | ||||
| 	setData (data) { | ||||
| 		this.data = data; | ||||
| 		this.name = data.name; | ||||
| 		this.identifier = data.identifier; | ||||
| 		this.hidden = false; | ||||
| 		this.hasAnimateIn = false; | ||||
| 		this.hasAnimateOut = false; | ||||
|  | ||||
| 		this.setConfig(data.config, data.configDeepMerge); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Set the module config and combine it with the module defaults. | ||||
| 	 * | ||||
| 	 * @param {object} config The combined module config. | ||||
| 	 * @param {boolean} deep Merge module config in deep. | ||||
| 	 */ | ||||
| 	setConfig: function (config, deep) { | ||||
| 	setConfig (config, deep) { | ||||
| 		this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns a socket object. If it doesn't exist, it's created. | ||||
| 	 * It also registers the notification callback. | ||||
| 	 * | ||||
| 	 * @returns {MMSocket} a socket object | ||||
| 	 */ | ||||
| 	socket: function () { | ||||
| 	socket () { | ||||
| 		if (typeof this._socket === "undefined") { | ||||
| 			this._socket = new MMSocket(this.name); | ||||
| 		} | ||||
| @@ -250,62 +245,56 @@ 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("//", "/"); | ||||
| 	file (file) { | ||||
| 		return `${this.data.path}/${file}`.replace("//", "/"); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Load all required stylesheets by requesting the MM object to load the files. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadStyles: function (callback) { | ||||
| 		this.loadDependencies("getStyles", callback); | ||||
| 	loadStyles () { | ||||
| 		return this.loadDependencies("getStyles"); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Load all required scripts by requesting the MM object to load the files. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadScripts: function (callback) { | ||||
| 		this.loadDependencies("getScripts", callback); | ||||
| 	loadScripts () { | ||||
| 		return this.loadDependencies("getScripts"); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Helper method to load all dependencies. | ||||
| 	 * | ||||
| 	 * @param {string} funcName Function name to call to get scripts or styles. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadDependencies: function (funcName, callback) { | ||||
| 	async loadDependencies (funcName) { | ||||
| 		let dependencies = this[funcName](); | ||||
|  | ||||
| 		const loadNextDependency = () => { | ||||
| 		const loadNextDependency = async () => { | ||||
| 			if (dependencies.length > 0) { | ||||
| 				const nextDependency = dependencies[0]; | ||||
| 				Loader.loadFile(nextDependency, this, () => { | ||||
| 					dependencies = dependencies.slice(1); | ||||
| 					loadNextDependency(); | ||||
| 				}); | ||||
| 				await Loader.loadFileForModule(nextDependency, this); | ||||
| 				dependencies = dependencies.slice(1); | ||||
| 				await loadNextDependency(); | ||||
| 			} else { | ||||
| 				callback(); | ||||
| 				return Promise.resolve(); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		loadNextDependency(); | ||||
| 		await loadNextDependency(); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Load all translations. | ||||
| 	 * | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<void>} | ||||
| 	 */ | ||||
| 	loadTranslations(callback) { | ||||
| 	async loadTranslations () { | ||||
| 		const translations = this.getTranslations() || {}; | ||||
| 		const language = config.language.toLowerCase(); | ||||
|  | ||||
| @@ -313,7 +302,6 @@ var Module = Class.extend({ | ||||
| 		const fallbackLanguage = languages[0]; | ||||
|  | ||||
| 		if (languages.length === 0) { | ||||
| 			callback(); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| @@ -321,28 +309,24 @@ var Module = Class.extend({ | ||||
| 		const translationsFallbackFile = translations[fallbackLanguage]; | ||||
|  | ||||
| 		if (!translationFile) { | ||||
| 			Translator.load(this, translationsFallbackFile, true, callback); | ||||
| 			return; | ||||
| 			return Translator.load(this, translationsFallbackFile, true); | ||||
| 		} | ||||
|  | ||||
| 		Translator.load(this, translationFile, false, () => { | ||||
| 			if (translationFile !== translationsFallbackFile) { | ||||
| 				Translator.load(this, translationsFallbackFile, true, callback); | ||||
| 			} else { | ||||
| 				callback(); | ||||
| 			} | ||||
| 		}); | ||||
| 		await Translator.load(this, translationFile, false); | ||||
|  | ||||
| 		if (translationFile !== translationsFallbackFile) { | ||||
| 			return Translator.load(this, translationsFallbackFile, true); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Request the translation for a given key with optional variables and default value. | ||||
| 	 * | ||||
| 	 * @param {string} key The key of the string to translate | ||||
| 	 * @param {string|object} [defaultValueOrVariables] The default value or variables for translating. | ||||
| 	 * @param {string} [defaultValue] The default value with variables. | ||||
| 	 * @returns {string} the translated key | ||||
| 	 */ | ||||
| 	translate: function (key, defaultValueOrVariables, defaultValue) { | ||||
| 	translate (key, defaultValueOrVariables, defaultValue) { | ||||
| 		if (typeof defaultValueOrVariables === "object") { | ||||
| 			return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || ""; | ||||
| 		} | ||||
| @@ -351,90 +335,87 @@ 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 (updateOptions) { | ||||
| 		MM.updateDom(this, updateOptions); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Send a notification to all modules. | ||||
| 	 * | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 */ | ||||
| 	sendNotification: function (notification, payload) { | ||||
| 	sendNotification (notification, payload) { | ||||
| 		MM.sendNotification(notification, payload, this); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Send a socket notification to the node helper. | ||||
| 	 * | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*} payload The payload of the notification. | ||||
| 	 */ | ||||
| 	sendSocketNotification: function (notification, payload) { | ||||
| 	sendSocketNotification (notification, payload) { | ||||
| 		this.socket().sendNotification(notification, payload); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Hide this module. | ||||
| 	 * | ||||
| 	 * @param {number} speed The speed of the hide animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the hide method. | ||||
| 	 */ | ||||
| 	hide: function (speed, callback, options) { | ||||
| 		if (typeof callback === "object") { | ||||
| 			options = callback; | ||||
| 			callback = function () {}; | ||||
| 		} | ||||
| 	hide (speed, callback, options = {}) { | ||||
| 		let usedCallback = callback || function () {}; | ||||
| 		let usedOptions = options; | ||||
|  | ||||
| 		callback = callback || function () {}; | ||||
| 		options = options || {}; | ||||
| 		if (typeof callback === "object") { | ||||
| 			Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!"); | ||||
| 			usedOptions = callback; | ||||
| 			usedCallback = function () {}; | ||||
| 		} | ||||
|  | ||||
| 		MM.hideModule( | ||||
| 			this, | ||||
| 			speed, | ||||
| 			() => { | ||||
| 				this.suspend(); | ||||
| 				callback(); | ||||
| 				usedCallback(); | ||||
| 			}, | ||||
| 			options | ||||
| 			usedOptions | ||||
| 		); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Show this module. | ||||
| 	 * | ||||
| 	 * @param {number} speed The speed of the show animation. | ||||
| 	 * @param {Function} callback Called when the animation is done. | ||||
| 	 * @param {object} [options] Optional settings for the show method. | ||||
| 	 */ | ||||
| 	show: function (speed, callback, options) { | ||||
| 		if (typeof callback === "object") { | ||||
| 			options = callback; | ||||
| 			callback = function () {}; | ||||
| 		} | ||||
| 	show (speed, callback, options) { | ||||
| 		let usedCallback = callback || function () {}; | ||||
| 		let usedOptions = options; | ||||
|  | ||||
| 		callback = callback || function () {}; | ||||
| 		options = options || {}; | ||||
| 		if (typeof callback === "object") { | ||||
| 			Log.error("Parameter mismatch in module.show: callback is not an optional parameter!"); | ||||
| 			usedOptions = callback; | ||||
| 			usedCallback = function () {}; | ||||
| 		} | ||||
|  | ||||
| 		MM.showModule( | ||||
| 			this, | ||||
| 			speed, | ||||
| 			() => { | ||||
| 				this.resume(); | ||||
| 				callback(); | ||||
| 				usedCallback(); | ||||
| 			}, | ||||
| 			options | ||||
| 			usedOptions | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Merging MagicMirror (or other) default/config script by @bugsounet | ||||
|  * Merging MagicMirror² (or other) default/config script by @bugsounet | ||||
|  * Merge 2 objects or/with array | ||||
|  * | ||||
|  * Usage: | ||||
| @@ -452,11 +433,10 @@ 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) { | ||||
| function configMerge (result) { | ||||
| 	const stack = Array.prototype.slice.call(arguments, 1); | ||||
| 	let item, key; | ||||
|  | ||||
| @@ -498,27 +478,28 @@ Module.create = function (name) { | ||||
|  | ||||
| Module.register = function (name, moduleDefinition) { | ||||
| 	if (moduleDefinition.requiresVersion) { | ||||
| 		Log.log("Check MagicMirror version for module '" + name + "' - Minimum version:  " + moduleDefinition.requiresVersion + " - Current version: " + window.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) { | ||||
| function cmpVersions (a, b) { | ||||
| 	const regExStrip0 = /(\.0+)+$/; | ||||
| 	const segmentsA = a.replace(regExStrip0, "").split("."); | ||||
| 	const segmentsB = b.replace(regExStrip0, "").split("."); | ||||
|   | ||||
| @@ -1,113 +1,94 @@ | ||||
| /* Magic Mirror | ||||
|  * Node Helper Superclass | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const Class = require("./class.js"); | ||||
| const Log = require("logger"); | ||||
| const express = require("express"); | ||||
| const Log = require("logger"); | ||||
| const Class = require("./class"); | ||||
|  | ||||
| const NodeHelper = Class.extend({ | ||||
| 	init() { | ||||
| 	init () { | ||||
| 		Log.log("Initializing new module helper ..."); | ||||
| 	}, | ||||
|  | ||||
| 	loaded(callback) { | ||||
| 	loaded () { | ||||
| 		Log.log(`Module helper loaded: ${this.name}`); | ||||
| 		callback(); | ||||
| 	}, | ||||
|  | ||||
| 	start() { | ||||
| 	start () { | ||||
| 		Log.log(`Starting module helper: ${this.name}`); | ||||
| 	}, | ||||
|  | ||||
| 	/* stop() | ||||
| 	 * Called when the MagicMirror server receives a `SIGINT` | ||||
| 	/** | ||||
| 	 * Called when the MagicMirror² server receives a `SIGINT` | ||||
| 	 * Close any open connections, stop any sub-processes and | ||||
| 	 * gracefully exit the module. | ||||
| 	 * | ||||
| 	 */ | ||||
| 	stop() { | ||||
| 	stop () { | ||||
| 		Log.log(`Stopping module helper: ${this.name}`); | ||||
| 	}, | ||||
|  | ||||
| 	/* socketNotificationReceived(notification, payload) | ||||
| 	/** | ||||
| 	 * This method is called when a socket notification arrives. | ||||
| 	 * | ||||
| 	 * argument notification string - The identifier of the notification. | ||||
| 	 * argument payload mixed - The payload of the notification. | ||||
| 	 * @param {string} notification The identifier of the notification. | ||||
| 	 * @param {*}  payload The payload of the notification. | ||||
| 	 */ | ||||
| 	socketNotificationReceived(notification, payload) { | ||||
| 	socketNotificationReceived (notification, payload) { | ||||
| 		Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`); | ||||
| 	}, | ||||
|  | ||||
| 	/* setName(name) | ||||
| 	/** | ||||
| 	 * Set the module name. | ||||
| 	 * | ||||
| 	 * argument name string - Module name. | ||||
| 	 * @param {string} name Module name. | ||||
| 	 */ | ||||
| 	setName(name) { | ||||
| 	setName (name) { | ||||
| 		this.name = name; | ||||
| 	}, | ||||
|  | ||||
| 	/* setPath(path) | ||||
| 	/** | ||||
| 	 * Set the module path. | ||||
| 	 * | ||||
| 	 * argument path string - Module path. | ||||
| 	 * @param {string} path Module path. | ||||
| 	 */ | ||||
| 	setPath(path) { | ||||
| 	setPath (path) { | ||||
| 		this.path = path; | ||||
| 	}, | ||||
|  | ||||
| 	/* sendSocketNotification(notification, payload) | ||||
| 	/* | ||||
| 	 * sendSocketNotification(notification, payload) | ||||
| 	 * Send a socket notification to the node helper. | ||||
| 	 * | ||||
| 	 * argument notification string - The identifier of the notification. | ||||
| 	 * argument payload mixed - The payload of the notification. | ||||
| 	 */ | ||||
| 	sendSocketNotification(notification, payload) { | ||||
| 	sendSocketNotification (notification, payload) { | ||||
| 		this.io.of(this.name).emit(notification, payload); | ||||
| 	}, | ||||
|  | ||||
| 	/* setExpressApp(app) | ||||
| 	/* | ||||
| 	 * setExpressApp(app) | ||||
| 	 * Sets the express app object for this module. | ||||
| 	 * This allows you to host files from the created webserver. | ||||
| 	 * | ||||
| 	 * argument app Express app - The Express app object. | ||||
| 	 */ | ||||
| 	setExpressApp(app) { | ||||
| 	setExpressApp (app) { | ||||
| 		this.expressApp = app; | ||||
|  | ||||
| 		app.use(`/${this.name}`, express.static(`${this.path}/public`)); | ||||
| 	}, | ||||
|  | ||||
| 	/* setSocketIO(io) | ||||
| 	/* | ||||
| 	 * setSocketIO(io) | ||||
| 	 * Sets the socket io object for this module. | ||||
| 	 * Binds message receiver. | ||||
| 	 * | ||||
| 	 * argument io Socket.io - The Socket io object. | ||||
| 	 */ | ||||
| 	setSocketIO(io) { | ||||
| 	setSocketIO (io) { | ||||
| 		this.io = io; | ||||
|  | ||||
| 		Log.log(`Connecting socket for: ${this.name}`); | ||||
|  | ||||
| 		io.of(this.name).on("connection", (socket) => { | ||||
| 			// add a catch all event. | ||||
| 			const onevent = socket.onevent; | ||||
| 			socket.onevent = function (packet) { | ||||
| 				const args = packet.data || []; | ||||
| 				onevent.call(this, packet); // original call | ||||
| 				packet.data = ["*"].concat(args); | ||||
| 				onevent.call(this, packet); // additional call to catch-all | ||||
| 			}; | ||||
|  | ||||
| 			// register catch all. | ||||
| 			socket.on("*", (notification, payload) => { | ||||
| 				if (notification !== "*") { | ||||
| 					this.socketNotificationReceived(notification, payload); | ||||
| 				} | ||||
| 			socket.onAny((notification, payload) => { | ||||
| 				this.socketNotificationReceived(notification, payload); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| @@ -125,7 +106,6 @@ NodeHelper.checkFetchStatus = function (response) { | ||||
| /** | ||||
|  * Look at the specified error and return an appropriate error type, that | ||||
|  * can be translated to a detailed error message | ||||
|  * | ||||
|  * @param {Error} error the error from fetching something | ||||
|  * @returns {string} the string of the detailed error message in the translations | ||||
|  */ | ||||
|   | ||||
							
								
								
									
										180
									
								
								js/server.js
									
									
									
									
									
								
							
							
						
						
									
										180
									
								
								js/server.js
									
									
									
									
									
								
							| @@ -1,97 +1,121 @@ | ||||
| /* Magic Mirror | ||||
|  * Server | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const fs = require("node:fs"); | ||||
| const http = require("node:http"); | ||||
| const https = require("node:https"); | ||||
| const path = require("node:path"); | ||||
| const express = require("express"); | ||||
| const app = require("express")(); | ||||
| const path = require("path"); | ||||
| const ipfilter = require("express-ipfilter").IpFilter; | ||||
| const fs = require("fs"); | ||||
| const helmet = require("helmet"); | ||||
|  | ||||
| const socketio = require("socket.io"); | ||||
| const Log = require("logger"); | ||||
| const Utils = require("./utils.js"); | ||||
| const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions"); | ||||
|  | ||||
| const vendor = require(`${__dirname}/vendor`); | ||||
|  | ||||
| /** | ||||
|  * Server | ||||
|  * | ||||
|  * @param {object} config The MM config | ||||
|  * @param {Function} callback Function called when done. | ||||
|  * @class | ||||
|  */ | ||||
| function Server(config, callback) { | ||||
| function Server (config) { | ||||
| 	const app = express(); | ||||
| 	const port = process.env.MM_PORT || config.port; | ||||
|  | ||||
| 	const serverSockets = new Set(); | ||||
| 	let server = null; | ||||
| 	if (config.useHttps) { | ||||
| 		const options = { | ||||
| 			key: fs.readFileSync(config.httpsPrivateKey), | ||||
| 			cert: fs.readFileSync(config.httpsCertificate) | ||||
| 		}; | ||||
| 		server = require("https").Server(options, app); | ||||
| 	} else { | ||||
| 		server = require("http").Server(app); | ||||
| 	} | ||||
| 	const io = require("socket.io")(server, { | ||||
| 		cors: { | ||||
| 			origin: /.*$/, | ||||
| 			credentials: true | ||||
| 		}, | ||||
| 		allowEIO3: true | ||||
| 	}); | ||||
|  | ||||
| 	Log.log(`Starting server on port ${port} ... `); | ||||
|  | ||||
| 	server.listen(port, config.address || "localhost"); | ||||
|  | ||||
| 	if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) { | ||||
| 		Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs")); | ||||
| 	} | ||||
|  | ||||
| 	app.use(function (req, res, next) { | ||||
| 		ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) { | ||||
| 			if (err === undefined) { | ||||
| 				return next(); | ||||
| 	/** | ||||
| 	 * Opens the server for incoming connections | ||||
| 	 * @returns {Promise} A promise that is resolved when the server listens to connections | ||||
| 	 */ | ||||
| 	this.open = function () { | ||||
| 		return new Promise((resolve) => { | ||||
| 			if (config.useHttps) { | ||||
| 				const options = { | ||||
| 					key: fs.readFileSync(config.httpsPrivateKey), | ||||
| 					cert: fs.readFileSync(config.httpsCertificate) | ||||
| 				}; | ||||
| 				server = https.Server(options, app); | ||||
| 			} else { | ||||
| 				server = http.Server(app); | ||||
| 			} | ||||
| 			Log.log(err.message); | ||||
| 			res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this."); | ||||
| 			const io = socketio(server, { | ||||
| 				cors: { | ||||
| 					origin: /.*$/, | ||||
| 					credentials: true | ||||
| 				}, | ||||
| 				allowEIO3: true | ||||
| 			}); | ||||
|  | ||||
| 			server.on("connection", (socket) => { | ||||
| 				serverSockets.add(socket); | ||||
| 				socket.on("close", () => { | ||||
| 					serverSockets.delete(socket); | ||||
| 				}); | ||||
| 			}); | ||||
|  | ||||
| 			Log.log(`Starting server on port ${port} ... `); | ||||
| 			server.listen(port, config.address || "localhost"); | ||||
|  | ||||
| 			if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) { | ||||
| 				Log.warn("You're using a full whitelist configuration to allow for all IPs"); | ||||
| 			} | ||||
|  | ||||
| 			app.use(function (req, res, next) { | ||||
| 				ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) { | ||||
| 					if (err === undefined) { | ||||
| 						res.header("Access-Control-Allow-Origin", "*"); | ||||
| 						return next(); | ||||
| 					} | ||||
| 					Log.log(err.message); | ||||
| 					res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this."); | ||||
| 				}); | ||||
| 			}); | ||||
|  | ||||
| 			app.use(helmet(config.httpHeaders)); | ||||
| 			app.use("/js", express.static(__dirname)); | ||||
|  | ||||
| 			let directories = ["/config", "/css", "/modules", "/node_modules/animate.css", "/node_modules/@fontsource", "/node_modules/@fortawesome", "/translations", "/tests/configs", "/tests/mocks"]; | ||||
| 			for (const [key, value] of Object.entries(vendor)) { | ||||
| 				const dirArr = value.split("/"); | ||||
| 				if (dirArr[0] === "node_modules") directories.push(`/${dirArr[0]}/${dirArr[1]}`); | ||||
| 			} | ||||
| 			const uniqDirs = [...new Set(directories)]; | ||||
| 			for (const directory of uniqDirs) { | ||||
| 				app.use(directory, express.static(path.resolve(global.root_path + directory))); | ||||
| 			} | ||||
|  | ||||
| 			app.get("/cors", async (req, res) => await cors(req, res)); | ||||
|  | ||||
| 			app.get("/version", (req, res) => getVersion(req, res)); | ||||
|  | ||||
| 			app.get("/config", (req, res) => getConfig(req, res)); | ||||
|  | ||||
| 			app.get("/startup", (req, res) => getStartup(req, res)); | ||||
|  | ||||
| 			app.get("/env", (req, res) => getEnvVars(req, res)); | ||||
|  | ||||
| 			app.get("/", (req, res) => getHtml(req, res)); | ||||
|  | ||||
| 			server.on("listening", () => { | ||||
| 				resolve({ | ||||
| 					app, | ||||
| 					io | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| 	app.use(helmet({ contentSecurityPolicy: false })); | ||||
| 	}; | ||||
|  | ||||
| 	app.use("/js", express.static(__dirname)); | ||||
|  | ||||
| 	const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"]; | ||||
| 	for (const directory of directories) { | ||||
| 		app.use(directory, express.static(path.resolve(global.root_path + directory))); | ||||
| 	} | ||||
|  | ||||
| 	app.get("/version", function (req, res) { | ||||
| 		res.send(global.version); | ||||
| 	}); | ||||
|  | ||||
| 	app.get("/config", function (req, res) { | ||||
| 		res.send(config); | ||||
| 	}); | ||||
|  | ||||
| 	app.get("/", function (req, res) { | ||||
| 		let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" }); | ||||
| 		html = html.replace("#VERSION#", global.version); | ||||
|  | ||||
| 		let configFile = "config/config.js"; | ||||
| 		if (typeof global.configuration_file !== "undefined") { | ||||
| 			configFile = global.configuration_file; | ||||
| 		} | ||||
| 		html = html.replace("#CONFIG_FILE#", configFile); | ||||
|  | ||||
| 		res.send(html); | ||||
| 	}); | ||||
|  | ||||
| 	if (typeof callback === "function") { | ||||
| 		callback(app, io); | ||||
| 	} | ||||
| 	/** | ||||
| 	 * Closes the server and destroys all lingering connections to it. | ||||
| 	 * @returns {Promise} A promise that resolves when server has successfully shut down | ||||
| 	 */ | ||||
| 	this.close = function () { | ||||
| 		return new Promise((resolve) => { | ||||
| 			for (const socket of serverSockets.values()) { | ||||
| 				socket.destroy(); | ||||
| 			} | ||||
| 			server.close(resolve); | ||||
| 		}); | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| module.exports = Server; | ||||
|   | ||||
							
								
								
									
										158
									
								
								js/server_functions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								js/server_functions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| const fs = require("node:fs"); | ||||
| const path = require("node:path"); | ||||
| const Log = require("logger"); | ||||
|  | ||||
| const startUp = new Date(); | ||||
|  | ||||
| /** | ||||
|  * Gets the config. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getConfig (req, res) { | ||||
| 	res.send(config); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the startup time. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getStartup (req, res) { | ||||
| 	res.send(startUp); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A method that forwards HTTP Get-methods to the internet to avoid CORS-errors. | ||||
|  * | ||||
|  * Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1 | ||||
|  * | ||||
|  * Only the url-param of the input request url is required. It must be the last parameter. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| async function cors (req, res) { | ||||
| 	try { | ||||
| 		const urlRegEx = "url=(.+?)$"; | ||||
| 		let url; | ||||
|  | ||||
| 		const match = new RegExp(urlRegEx, "g").exec(req.url); | ||||
| 		if (!match) { | ||||
| 			url = `invalid url: ${req.url}`; | ||||
| 			Log.error(url); | ||||
| 			res.send(url); | ||||
| 		} else { | ||||
| 			url = match[1]; | ||||
|  | ||||
| 			const headersToSend = getHeadersToSend(req.url); | ||||
| 			const expectedReceivedHeaders = geExpectedReceivedHeaders(req.url); | ||||
|  | ||||
| 			Log.log(`cors url: ${url}`); | ||||
| 			const response = await fetch(url, { headers: headersToSend }); | ||||
|  | ||||
| 			for (const header of expectedReceivedHeaders) { | ||||
| 				const headerValue = response.headers.get(header); | ||||
| 				if (header) res.set(header, headerValue); | ||||
| 			} | ||||
| 			const data = await response.text(); | ||||
| 			res.send(data); | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		Log.error(error); | ||||
| 		res.send(error); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets headers and values to attach to the web request. | ||||
|  * @param {string} url - The url containing the headers and values to send. | ||||
|  * @returns {object} An object specifying name and value of the headers. | ||||
|  */ | ||||
| function getHeadersToSend (url) { | ||||
| 	const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` }; | ||||
| 	const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url); | ||||
| 	if (headersToSendMatch) { | ||||
| 		const headers = headersToSendMatch[1].split(","); | ||||
| 		for (const header of headers) { | ||||
| 			const keyValue = header.split(":"); | ||||
| 			if (keyValue.length !== 2) { | ||||
| 				throw new Error(`Invalid format for header ${header}`); | ||||
| 			} | ||||
| 			headersToSend[keyValue[0]] = decodeURIComponent(keyValue[1]); | ||||
| 		} | ||||
| 	} | ||||
| 	return headersToSend; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the headers expected from the response. | ||||
|  * @param {string} url - The url containing the expected headers from the response. | ||||
|  * @returns {string[]} headers - The name of the expected headers. | ||||
|  */ | ||||
| function geExpectedReceivedHeaders (url) { | ||||
| 	const expectedReceivedHeaders = ["Content-Type"]; | ||||
| 	const expectedReceivedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url); | ||||
| 	if (expectedReceivedHeadersMatch) { | ||||
| 		const headers = expectedReceivedHeadersMatch[1].split(","); | ||||
| 		for (const header of headers) { | ||||
| 			expectedReceivedHeaders.push(header); | ||||
| 		} | ||||
| 	} | ||||
| 	return expectedReceivedHeaders; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the HTML to display the magic mirror. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getHtml (req, res) { | ||||
| 	let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" }); | ||||
| 	html = html.replace("#VERSION#", global.version); | ||||
| 	html = html.replace("#TESTMODE#", global.mmTestMode); | ||||
|  | ||||
| 	let configFile = "config/config.js"; | ||||
| 	if (typeof global.configuration_file !== "undefined") { | ||||
| 		configFile = global.configuration_file; | ||||
| 	} | ||||
| 	html = html.replace("#CONFIG_FILE#", configFile); | ||||
|  | ||||
| 	res.send(html); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the MagicMirror version. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getVersion (req, res) { | ||||
| 	res.send(global.version); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets environment variables needed in the browser. | ||||
|  * @returns {object} environment variables key: values | ||||
|  */ | ||||
| function getEnvVarsAsObj () { | ||||
| 	const obj = { modulesDir: `${config.foreignModulesDir}`, customCss: `${config.customCss}` }; | ||||
| 	if (process.env.MM_MODULES_DIR) { | ||||
| 		obj.modulesDir = process.env.MM_MODULES_DIR.replace(`${global.root_path}/`, ""); | ||||
| 	} | ||||
| 	if (process.env.MM_CUSTOMCSS_FILE) { | ||||
| 		obj.customCss = process.env.MM_CUSTOMCSS_FILE.replace(`${global.root_path}/`, ""); | ||||
| 	} | ||||
|  | ||||
| 	return obj; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets environment variables needed in the browser. | ||||
|  * @param {Request} req - the request | ||||
|  * @param {Response} res - the result | ||||
|  */ | ||||
| function getEnvVars (req, res) { | ||||
| 	const obj = getEnvVarsAsObj(); | ||||
| 	res.send(obj); | ||||
| } | ||||
|  | ||||
| module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj }; | ||||
| @@ -1,11 +1,5 @@ | ||||
| /* global io */ | ||||
|  | ||||
| /* Magic Mirror | ||||
|  * TODO add description | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const MMSocket = function (moduleName) { | ||||
| 	if (typeof moduleName !== "string") { | ||||
| 		throw new Error("Please set the module name for the MMSocket."); | ||||
| @@ -18,8 +12,8 @@ const MMSocket = function (moduleName) { | ||||
| 	if (typeof config !== "undefined" && typeof config.basePath !== "undefined") { | ||||
| 		base = config.basePath; | ||||
| 	} | ||||
| 	this.socket = io("/" + this.moduleName, { | ||||
| 		path: base + "socket.io" | ||||
| 	this.socket = io(`/${this.moduleName}`, { | ||||
| 		path: `${base}socket.io` | ||||
| 	}); | ||||
|  | ||||
| 	let notificationCallback = function () {}; | ||||
| @@ -44,10 +38,7 @@ const MMSocket = function (moduleName) { | ||||
| 		notificationCallback = callback; | ||||
| 	}; | ||||
|  | ||||
| 	this.sendNotification = (notification, payload) => { | ||||
| 		if (typeof payload === "undefined") { | ||||
| 			payload = {}; | ||||
| 		} | ||||
| 	this.sendNotification = (notification, payload = {}) => { | ||||
| 		this.socket.emit(notification, payload); | ||||
| 	}; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										107
									
								
								js/translator.js
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								js/translator.js
									
									
									
									
									
								
							| @@ -1,36 +1,32 @@ | ||||
| /* global translations */ | ||||
|  | ||||
| /* Magic Mirror | ||||
|  * Translator (l10n) | ||||
|  * | ||||
|  * By Christopher Fenner https://github.com/CFenner | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| var Translator = (function () { | ||||
| const Translator = (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Load a JSON file via XHR. | ||||
| 	 * | ||||
| 	 * @param {string} file Path of the file we want to load. | ||||
| 	 * @param {Function} callback Function called when done. | ||||
| 	 * @returns {Promise<object>} the translations in the specified file | ||||
| 	 */ | ||||
| 	function loadJSON(file, callback) { | ||||
| 	async function loadJSON (file) { | ||||
| 		const xhr = new XMLHttpRequest(); | ||||
| 		xhr.overrideMimeType("application/json"); | ||||
| 		xhr.open("GET", file, true); | ||||
| 		xhr.onreadystatechange = function () { | ||||
| 			if (xhr.readyState === 4 && xhr.status === 200) { | ||||
| 				// needs error handler try/catch at least | ||||
| 				let fileinfo = null; | ||||
| 				try { | ||||
| 					fileinfo = JSON.parse(xhr.responseText); | ||||
| 				} catch (exception) { | ||||
| 					// nothing here, but don't die | ||||
| 					Log.error(" loading json file =" + file + " failed"); | ||||
| 		return new Promise(function (resolve) { | ||||
| 			xhr.overrideMimeType("application/json"); | ||||
| 			xhr.open("GET", file, true); | ||||
| 			xhr.onreadystatechange = function () { | ||||
| 				if (xhr.readyState === 4 && xhr.status === 200) { | ||||
| 					// needs error handler try/catch at least | ||||
| 					let fileInfo = null; | ||||
| 					try { | ||||
| 						fileInfo = JSON.parse(xhr.responseText); | ||||
| 					} catch (exception) { | ||||
| 						// nothing here, but don't die | ||||
| 						Log.error(` loading json file =${file} failed`); | ||||
| 					} | ||||
| 					resolve(fileInfo); | ||||
| 				} | ||||
| 				callback(fileinfo); | ||||
| 			} | ||||
| 		}; | ||||
| 		xhr.send(null); | ||||
| 			}; | ||||
| 			xhr.send(null); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	return { | ||||
| @@ -41,34 +37,32 @@ 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 (module, key, variables = {}) { | ||||
|  | ||||
| 			/** | ||||
| 			 * Combines template and variables like: | ||||
| 			 * template: "Please wait for {timeToWait} before continuing with {work}." | ||||
| 			 * variables: {timeToWait: "2 hours", work: "painting"} | ||||
| 			 * to: "Please wait for 2 hours before continuing with painting." | ||||
| 			 * | ||||
| 			 * @param {string} template Text with placeholder | ||||
| 			 * @param {object} variables Variables for the placeholder | ||||
| 			 * @returns {string} the template filled with the variables | ||||
| 			 */ | ||||
| 			function createStringFromTemplate(template, variables) { | ||||
| 			function createStringFromTemplate (template, variables) { | ||||
| 				if (Object.prototype.toString.call(template) !== "[object String]") { | ||||
| 					return template; | ||||
| 				} | ||||
| 				let templateToUse = template; | ||||
| 				if (variables.fallback && !template.match(new RegExp("{.+}"))) { | ||||
| 					template = variables.fallback; | ||||
| 					templateToUse = variables.fallback; | ||||
| 				} | ||||
| 				return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) { | ||||
| 					return varName in variables ? variables[varName] : "{" + varName + "}"; | ||||
| 				return templateToUse.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) { | ||||
| 					return varName in variables ? variables[varName] : `{${varName}}`; | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| @@ -97,62 +91,49 @@ var Translator = (function () { | ||||
|  | ||||
| 		/** | ||||
| 		 * Load a translation file (json) and remember the data. | ||||
| 		 * | ||||
| 		 * @param {Module} module The module to load the translation file for. | ||||
| 		 * @param {string} file Path of the file we want to load. | ||||
| 		 * @param {boolean} isFallback Flag to indicate fallback translations. | ||||
| 		 * @param {Function} callback Function called when done. | ||||
| 		 */ | ||||
| 		load(module, file, isFallback, callback) { | ||||
| 			Log.log(`${module.name} - Load translation${isFallback && " fallback"}: ${file}`); | ||||
| 		async load (module, file, isFallback) { | ||||
| 			Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`); | ||||
|  | ||||
| 			if (this.translationsFallback[module.name]) { | ||||
| 				callback(); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			loadJSON(module.file(file), (json) => { | ||||
| 				const property = isFallback ? "translationsFallback" : "translations"; | ||||
| 				this[property][module.name] = json; | ||||
| 				callback(); | ||||
| 			}); | ||||
| 			const json = await loadJSON(module.file(file)); | ||||
| 			const property = isFallback ? "translationsFallback" : "translations"; | ||||
| 			this[property][module.name] = json; | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Load the core translations. | ||||
| 		 * | ||||
| 		 * @param {string} lang The language identifier of the core language. | ||||
| 		 */ | ||||
| 		loadCoreTranslations: function (lang) { | ||||
| 		async loadCoreTranslations (lang) { | ||||
| 			if (lang in translations) { | ||||
| 				Log.log("Loading core translation file: " + translations[lang]); | ||||
| 				loadJSON(translations[lang], (translations) => { | ||||
| 					this.coreTranslations = translations; | ||||
| 				}); | ||||
| 				Log.log(`Loading core translation file: ${translations[lang]}`); | ||||
| 				this.coreTranslations = await loadJSON(translations[lang]); | ||||
| 			} else { | ||||
| 				Log.log("Configured language not found in core translations."); | ||||
| 			} | ||||
|  | ||||
| 			this.loadCoreTranslationsFallback(); | ||||
| 			await this.loadCoreTranslationsFallback(); | ||||
| 		}, | ||||
|  | ||||
| 		/** | ||||
| 		 * Load the core translations fallback. | ||||
| 		 * Load the core translations' fallback. | ||||
| 		 * The first language defined in translations.js will be used. | ||||
| 		 */ | ||||
| 		loadCoreTranslationsFallback: function () { | ||||
| 			// The variable `first` will contain the first | ||||
| 			// defined translation after the following line. | ||||
| 			for (var first in translations) { | ||||
| 				break; | ||||
| 			} | ||||
|  | ||||
| 		async loadCoreTranslationsFallback () { | ||||
| 			let first = Object.keys(translations)[0]; | ||||
| 			if (first) { | ||||
| 				Log.log("Loading core translation fallback file: " + translations[first]); | ||||
| 				loadJSON(translations[first], (translations) => { | ||||
| 					this.coreTranslationsFallback = translations; | ||||
| 				}); | ||||
| 				Log.log(`Loading core translation fallback file: ${translations[first]}`); | ||||
| 				this.coreTranslationsFallback = await loadJSON(translations[first]); | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| })(); | ||||
| }()); | ||||
|  | ||||
| window.Translator = Translator; | ||||
|   | ||||
							
								
								
									
										87
									
								
								js/utils.js
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								js/utils.js
									
									
									
									
									
								
							| @@ -1,16 +1,79 @@ | ||||
| /* Magic Mirror | ||||
|  * Utils | ||||
|  * | ||||
|  * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const colors = require("colors/safe"); | ||||
| const execSync = require("node:child_process").execSync; | ||||
| const path = require("node:path"); | ||||
|  | ||||
| const rootPath = path.resolve(`${__dirname}/../`); | ||||
| const Log = require(`${rootPath}/js/logger.js`); | ||||
| const os = require("node:os"); | ||||
| const fs = require("node:fs"); | ||||
| const si = require("systeminformation"); | ||||
|  | ||||
| const modulePositions = []; // will get list from index.html | ||||
| const regionRegEx = /"region ([^"]*)/i; | ||||
| const indexFileName = "index.html"; | ||||
| const discoveredPositionsJSFilename = "js/positions.js"; | ||||
|  | ||||
| module.exports = { | ||||
| 	colors: { | ||||
| 		warn: colors.yellow, | ||||
| 		error: colors.red, | ||||
| 		info: colors.blue, | ||||
| 		pass: colors.green | ||||
|  | ||||
| 	async logSystemInformation  () { | ||||
| 		try { | ||||
| 			let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, ""); | ||||
|  | ||||
| 			const staticData = await si.get({ | ||||
| 				system: "manufacturer, model, virtual", | ||||
| 				osInfo: "platform, distro, release, arch", | ||||
| 				versions: "kernel, node, npm, pm2" | ||||
| 			}); | ||||
| 			let systemDataString = `System information: | ||||
| 					### SYSTEM:   manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual} | ||||
| 					### OS:       platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel} | ||||
| 					### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2} | ||||
| 					### OTHER:    timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}` | ||||
| 				.replace(/\t/g, ""); | ||||
| 			Log.info(systemDataString); | ||||
|  | ||||
| 			// Return is currently only for jest | ||||
| 			return systemDataString; | ||||
| 		} catch (e) { | ||||
| 			Log.error(e); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	// return all available module positions | ||||
| 	getAvailableModulePositions () { | ||||
| 		return modulePositions; | ||||
| 	}, | ||||
|  | ||||
| 	// return if position is on modulePositions Array (true/false) | ||||
| 	moduleHasValidPosition (position) { | ||||
| 		if (this.getAvailableModulePositions().indexOf(position) === -1) return false; | ||||
| 		return true; | ||||
| 	}, | ||||
|  | ||||
| 	getModulePositions () { | ||||
| 		// if not already discovered | ||||
| 		if (modulePositions.length === 0) { | ||||
| 			// get the lines of the index.html | ||||
| 			const lines = fs.readFileSync(indexFileName).toString().split("\n"); | ||||
| 			// loop thru the lines | ||||
| 			lines.forEach((line) => { | ||||
| 				// run the regex on each line | ||||
| 				const results = regionRegEx.exec(line); | ||||
| 				// if the regex returned something | ||||
| 				if (results && results.length > 0) { | ||||
| 					// get the position parts and replace space with underscore | ||||
| 					const positionName = results[1].replace(" ", "_"); | ||||
| 					// add it to the list | ||||
| 					modulePositions.push(positionName); | ||||
| 				} | ||||
| 			}); | ||||
| 			try { | ||||
| 				fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`); | ||||
| 			} | ||||
| 			catch (error) { | ||||
| 				Log.error("unable to write js/positions.js with the discovered module positions\nmake the MagicMirror/js folder writeable by the user starting MagicMirror"); | ||||
| 			} | ||||
| 		} | ||||
| 		// return the list to the caller | ||||
| 		return modulePositions; | ||||
| 	} | ||||
| }; | ||||
|   | ||||
							
								
								
									
										9
									
								
								vendor/vendor.js → js/vendor.js
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										9
									
								
								vendor/vendor.js → js/vendor.js
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,9 +1,3 @@ | ||||
| /* Magic Mirror | ||||
|  * Vendor File Definition | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl
 | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const vendor = { | ||||
| 	"moment.js": "node_modules/moment/min/moment-with-locales.js", | ||||
| 	"moment-timezone.js": "node_modules/moment-timezone/builds/moment-timezone-with-data.js", | ||||
| @@ -11,7 +5,8 @@ const vendor = { | ||||
| 	"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css", | ||||
| 	"font-awesome.css": "css/font-awesome.css", | ||||
| 	"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js", | ||||
| 	"suncalc.js": "node_modules/suncalc/suncalc.js" | ||||
| 	"suncalc.js": "node_modules/suncalc/suncalc.js", | ||||
| 	"croner.js": "node_modules/croner/dist/croner.umd.js" | ||||
| }; | ||||
| 
 | ||||
| if (typeof module !== "undefined") { | ||||
| @@ -1,16 +1,19 @@ | ||||
| type ModuleProperties = { | ||||
|   defaults?: object, | ||||
|   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,142 @@ | ||||
| /* global NotificationFx */ | ||||
|  | ||||
| /* Magic Mirror | ||||
|  * Module: alert | ||||
|  * | ||||
|  * By Paul-Vincent Roll https://paulvincentroll.com/ | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| Module.register("alert", { | ||||
| 	alerts: {}, | ||||
|  | ||||
| 	defaults: { | ||||
| 		// scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		effect: "slide", | ||||
| 		// scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		alert_effect: "jelly", | ||||
| 		//time a notification is displayed in seconds | ||||
| 		display_time: 3500, | ||||
| 		//Position | ||||
| 		effect: "slide", // scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		alert_effect: "jelly", // scale|slide|genie|jelly|flip|bouncyflip|exploader | ||||
| 		display_time: 3500, // time a notification is displayed in seconds | ||||
| 		position: "center", | ||||
| 		//shown at startup | ||||
| 		welcome_message: false | ||||
| 		welcome_message: false // shown at startup | ||||
| 	}, | ||||
| 	getScripts: function () { | ||||
|  | ||||
| 	getScripts () { | ||||
| 		return ["notificationFx.js"]; | ||||
| 	}, | ||||
| 	getStyles: function () { | ||||
| 		return ["notificationFx.css", "font-awesome.css"]; | ||||
|  | ||||
| 	getStyles () { | ||||
| 		return ["font-awesome.css", this.file("./styles/notificationFx.css"), this.file(`./styles/${this.config.position}.css`)]; | ||||
| 	}, | ||||
| 	// Define required translations. | ||||
| 	getTranslations: function () { | ||||
|  | ||||
| 	getTranslations () { | ||||
| 		return { | ||||
| 			en: "translations/en.json", | ||||
| 			bg: "translations/bg.json", | ||||
| 			da: "translations/da.json", | ||||
| 			de: "translations/de.json", | ||||
| 			nl: "translations/nl.json" | ||||
| 			en: "translations/en.json", | ||||
| 			eo: "translations/eo.json", | ||||
| 			es: "translations/es.json", | ||||
| 			fr: "translations/fr.json", | ||||
| 			hu: "translations/hu.json", | ||||
| 			nl: "translations/nl.json", | ||||
| 			ru: "translations/ru.json", | ||||
| 			th: "translations/th.json" | ||||
| 		}; | ||||
| 	}, | ||||
| 	show_notification: function (message) { | ||||
|  | ||||
| 	getTemplate (type) { | ||||
| 		return `templates/${type}.njk`; | ||||
| 	}, | ||||
|  | ||||
| 	async start () { | ||||
| 		Log.info(`Starting module: ${this.name}`); | ||||
|  | ||||
| 		if (this.config.effect === "slide") { | ||||
| 			this.config.effect = this.config.effect + "-" + this.config.position; | ||||
| 			this.config.effect = `${this.config.effect}-${this.config.position}`; | ||||
| 		} | ||||
| 		let msg = ""; | ||||
| 		if (message.title) { | ||||
| 			msg += "<span class='thin dimmed medium'>" + message.title + "</span>"; | ||||
|  | ||||
| 		if (this.config.welcome_message) { | ||||
| 			const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message; | ||||
| 			await this.showNotification({ title: this.translate("sysTitle"), message }); | ||||
| 		} | ||||
| 		if (message.message) { | ||||
| 			if (msg !== "") { | ||||
| 				msg += "<br />"; | ||||
| 	}, | ||||
|  | ||||
| 	notificationReceived (notification, payload, sender) { | ||||
| 		if (notification === "SHOW_ALERT") { | ||||
| 			if (payload.type === "notification") { | ||||
| 				this.showNotification(payload); | ||||
| 			} else { | ||||
| 				this.showAlert(payload, sender); | ||||
| 			} | ||||
| 			msg += "<span class='light bright small'>" + message.message + "</span>"; | ||||
| 		} else if (notification === "HIDE_ALERT") { | ||||
| 			this.hideAlert(sender); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	async showNotification (notification) { | ||||
| 		const message = await this.renderMessage(notification.templateName || "notification", notification); | ||||
|  | ||||
| 		new NotificationFx({ | ||||
| 			message: msg, | ||||
| 			message, | ||||
| 			layout: "growl", | ||||
| 			effect: this.config.effect, | ||||
| 			ttl: message.timer !== undefined ? message.timer : this.config.display_time | ||||
| 			ttl: notification.timer || this.config.display_time | ||||
| 		}).show(); | ||||
| 	}, | ||||
| 	show_alert: function (params, sender) { | ||||
| 		let image = ""; | ||||
| 		//Set standard params if not provided by module | ||||
| 		if (typeof params.timer === "undefined") { | ||||
| 			params.timer = null; | ||||
| 		} | ||||
| 		if (typeof params.imageHeight === "undefined") { | ||||
| 			params.imageHeight = "80px"; | ||||
| 		} | ||||
| 		if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") { | ||||
| 			params.imageUrl = null; | ||||
| 		} else if (typeof params.imageFA === "undefined") { | ||||
| 			image = "<img src='" + params.imageUrl.toString() + "' height='" + params.imageHeight.toString() + "' style='margin-bottom: 10px;'/><br />"; | ||||
| 		} else if (typeof params.imageUrl === "undefined") { | ||||
| 			image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + params.imageHeight.toString() + ";'/></span><br />"; | ||||
| 		} | ||||
| 		//Create overlay | ||||
| 		const overlay = document.createElement("div"); | ||||
| 		overlay.id = "overlay"; | ||||
| 		overlay.innerHTML += '<div class="black_overlay"></div>'; | ||||
| 		document.body.insertBefore(overlay, document.body.firstChild); | ||||
|  | ||||
| 		//If module already has an open alert close it | ||||
| 	async showAlert (alert, sender) { | ||||
| 		// If module already has an open alert close it | ||||
| 		if (this.alerts[sender.name]) { | ||||
| 			this.hide_alert(sender, false); | ||||
| 			this.hideAlert(sender, false); | ||||
| 		} | ||||
|  | ||||
| 		//Display title and message only if they are provided in notification parameters | ||||
| 		let message = ""; | ||||
| 		if (params.title) { | ||||
| 			message += "<span class='light dimmed medium'>" + params.title + "</span>"; | ||||
| 		} | ||||
| 		if (params.message) { | ||||
| 			if (message !== "") { | ||||
| 				message += "<br />"; | ||||
| 			} | ||||
|  | ||||
| 			message += "<span class='thin bright small'>" + params.message + "</span>"; | ||||
| 		// Add overlay | ||||
| 		if (!Object.keys(this.alerts).length) { | ||||
| 			this.toggleBlur(true); | ||||
| 		} | ||||
|  | ||||
| 		//Store alert in this.alerts | ||||
| 		const message = await this.renderMessage(alert.templateName || "alert", alert); | ||||
|  | ||||
| 		// Store alert in this.alerts | ||||
| 		this.alerts[sender.name] = new NotificationFx({ | ||||
| 			message: image + message, | ||||
| 			message, | ||||
| 			effect: this.config.alert_effect, | ||||
| 			ttl: params.timer, | ||||
| 			onClose: () => this.hide_alert(sender), | ||||
| 			ttl: alert.timer, | ||||
| 			onClose: () => this.hideAlert(sender), | ||||
| 			al_no: "ns-alert" | ||||
| 		}); | ||||
|  | ||||
| 		//Show alert | ||||
| 		// Show alert | ||||
| 		this.alerts[sender.name].show(); | ||||
|  | ||||
| 		//Add timer to dismiss alert and overlay | ||||
| 		if (params.timer) { | ||||
| 		// Add timer to dismiss alert and overlay | ||||
| 		if (alert.timer) { | ||||
| 			setTimeout(() => { | ||||
| 				this.hide_alert(sender); | ||||
| 			}, params.timer); | ||||
| 				this.hideAlert(sender); | ||||
| 			}, alert.timer); | ||||
| 		} | ||||
| 	}, | ||||
| 	hide_alert: function (sender, close = true) { | ||||
| 		//Dismiss alert and remove from this.alerts | ||||
|  | ||||
| 	hideAlert (sender, close = true) { | ||||
| 		// Dismiss alert and remove from this.alerts | ||||
| 		if (this.alerts[sender.name]) { | ||||
| 			this.alerts[sender.name].dismiss(close); | ||||
| 			this.alerts[sender.name] = null; | ||||
| 			//Remove overlay | ||||
| 			const overlay = document.getElementById("overlay"); | ||||
| 			overlay.parentNode.removeChild(overlay); | ||||
| 		} | ||||
| 	}, | ||||
| 	setPosition: function (pos) { | ||||
| 		//Add css to body depending on the set position for notifications | ||||
| 		const sheet = document.createElement("style"); | ||||
| 		if (pos === "center") { | ||||
| 			sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}"; | ||||
| 		} | ||||
| 		if (pos === "right") { | ||||
| 			sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}"; | ||||
| 		} | ||||
| 		if (pos === "left") { | ||||
| 			sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}"; | ||||
| 		} | ||||
| 		document.body.appendChild(sheet); | ||||
| 	}, | ||||
| 	notificationReceived: function (notification, payload, sender) { | ||||
| 		if (notification === "SHOW_ALERT") { | ||||
| 			if (typeof payload.type === "undefined") { | ||||
| 				payload.type = "alert"; | ||||
| 			} | ||||
| 			if (payload.type === "alert") { | ||||
| 				this.show_alert(payload, sender); | ||||
| 			} else if (payload.type === "notification") { | ||||
| 				this.show_notification(payload); | ||||
| 			} | ||||
| 		} else if (notification === "HIDE_ALERT") { | ||||
| 			this.hide_alert(sender); | ||||
| 		} | ||||
| 	}, | ||||
| 	start: function () { | ||||
| 		this.alerts = {}; | ||||
| 		this.setPosition(this.config.position); | ||||
| 		if (this.config.welcome_message) { | ||||
| 			if (this.config.welcome_message === true) { | ||||
| 				this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") }); | ||||
| 			} else { | ||||
| 				this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message }); | ||||
| 			delete this.alerts[sender.name]; | ||||
| 			// Remove overlay | ||||
| 			if (!Object.keys(this.alerts).length) { | ||||
| 				this.toggleBlur(false); | ||||
| 			} | ||||
| 		} | ||||
| 		Log.info("Starting module: " + this.name); | ||||
| 	}, | ||||
|  | ||||
| 	renderMessage (type, data) { | ||||
| 		return new Promise((resolve) => { | ||||
| 			this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) { | ||||
| 				if (err) { | ||||
| 					Log.error("Failed to render alert", err); | ||||
| 				} | ||||
|  | ||||
| 				resolve(res); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
|  | ||||
| 	toggleBlur (add = false) { | ||||
| 		const method = add ? "add" : "remove"; | ||||
| 		const modules = document.querySelectorAll(".module"); | ||||
| 		for (const module of modules) { | ||||
| 			module.classList[method]("alert-blur"); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -9,16 +9,17 @@ | ||||
|  * | ||||
|  * Copyright 2014, Codrops | ||||
|  * https://tympanus.net/codrops/ | ||||
|  * @param {object} window The window object | ||||
|  */ | ||||
| (function (window) { | ||||
|  | ||||
| 	/** | ||||
| 	 * Extend one object with another one | ||||
| 	 * | ||||
| 	 * @param {object} a The object to extend | ||||
| 	 * @param {object} b The object which extends the other, overwrites existing keys | ||||
| 	 * @returns {object} The merged object | ||||
| 	 */ | ||||
| 	function extend(a, b) { | ||||
| 	function extend (a, b) { | ||||
| 		for (let key in b) { | ||||
| 			if (b.hasOwnProperty(key)) { | ||||
| 				a[key] = b[key]; | ||||
| @@ -29,11 +30,10 @@ | ||||
|  | ||||
| 	/** | ||||
| 	 * NotificationFx constructor | ||||
| 	 * | ||||
| 	 * @param {object} options The configuration options | ||||
| 	 * @class | ||||
| 	 */ | ||||
| 	function NotificationFx(options) { | ||||
| 	function NotificationFx (options) { | ||||
| 		this.options = extend({}, this.options); | ||||
| 		extend(this.options, options); | ||||
| 		this._init(); | ||||
| @@ -64,10 +64,10 @@ | ||||
| 		ttl: 6000, | ||||
| 		al_no: "ns-box", | ||||
| 		// callbacks | ||||
| 		onClose: function () { | ||||
| 		onClose () { | ||||
| 			return false; | ||||
| 		}, | ||||
| 		onOpen: function () { | ||||
| 		onOpen () { | ||||
| 			return false; | ||||
| 		} | ||||
| 	}; | ||||
| @@ -78,8 +78,8 @@ | ||||
| 	NotificationFx.prototype._init = function () { | ||||
| 		// create HTML structure | ||||
| 		this.ntf = document.createElement("div"); | ||||
| 		this.ntf.className = this.options.al_no + " ns-" + this.options.layout + " ns-effect-" + this.options.effect + " ns-type-" + this.options.type; | ||||
| 		let strinner = '<div class="ns-box-inner">'; | ||||
| 		this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`; | ||||
| 		let strinner = "<div class=\"ns-box-inner\">"; | ||||
| 		strinner += this.options.message; | ||||
| 		strinner += "</div>"; | ||||
| 		this.ntf.innerHTML = strinner; | ||||
| @@ -122,7 +122,6 @@ | ||||
|  | ||||
| 	/** | ||||
| 	 * Dismiss the notification | ||||
| 	 * | ||||
| 	 * @param {boolean} [close] call the onClose callback at the end | ||||
| 	 */ | ||||
| 	NotificationFx.prototype.dismiss = function (close = true) { | ||||
| @@ -155,4 +154,4 @@ | ||||
| 	 * Add to global namespace | ||||
| 	 */ | ||||
| 	window.NotificationFx = NotificationFx; | ||||
| })(window); | ||||
| }(window)); | ||||
|   | ||||
							
								
								
									
										5
									
								
								modules/default/alert/styles/center.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								modules/default/alert/styles/center.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| .ns-box { | ||||
|   margin-left: auto; | ||||
|   margin-right: auto; | ||||
|   text-align: center; | ||||
| } | ||||
							
								
								
									
										4
									
								
								modules/default/alert/styles/left.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/styles/left.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| .ns-box { | ||||
|   margin-right: auto; | ||||
|   text-align: left; | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| /* Based on work by https://tympanus.net/codrops/licensing/ */ | ||||
| 
 | ||||
| .ns-box { | ||||
|   background-color: rgba(0, 0, 0, 0.93); | ||||
|   background-color: rgb(0 0 0 / 93%); | ||||
|   padding: 17px; | ||||
|   line-height: 1.4; | ||||
|   margin-bottom: 10px; | ||||
| @@ -39,12 +39,8 @@ | ||||
|   border-radius: 20px; | ||||
| } | ||||
| 
 | ||||
| .black_overlay { | ||||
|   position: fixed; | ||||
|   z-index: 2; | ||||
|   background-color: rgba(0, 0, 0, 0.93); | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| .alert-blur { | ||||
|   filter: blur(2px) brightness(50%); | ||||
| } | ||||
| 
 | ||||
| [class^="ns-effect-"].ns-growl.ns-hide, | ||||
| @@ -59,15 +55,15 @@ | ||||
| 
 | ||||
| .ns-effect-flip.ns-show, | ||||
| .ns-effect-flip.ns-hide { | ||||
|   animation-name: animFlipFront; | ||||
|   animation-name: anim-flip-front; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| .ns-effect-flip.ns-hide { | ||||
|   animation-name: animFlipBack; | ||||
|   animation-name: anim-flip-back; | ||||
| } | ||||
| 
 | ||||
| @keyframes animFlipFront { | ||||
| @keyframes anim-flip-front { | ||||
|   0% { | ||||
|     transform: perspective(1000px) rotate3d(1, 0, 0, -90deg); | ||||
|   } | ||||
| @@ -77,7 +73,7 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes animFlipBack { | ||||
| @keyframes anim-flip-back { | ||||
|   0% { | ||||
|     transform: perspective(1000px) rotate3d(1, 0, 0, 90deg); | ||||
|   } | ||||
| @@ -89,11 +85,11 @@ | ||||
| 
 | ||||
| .ns-effect-bouncyflip.ns-show, | ||||
| .ns-effect-bouncyflip.ns-hide { | ||||
|   animation-name: flipInX; | ||||
|   animation-name: flip-in-x; | ||||
|   animation-duration: 0.8s; | ||||
| } | ||||
| 
 | ||||
| @keyframes flipInX { | ||||
| @keyframes flip-in-x { | ||||
|   0% { | ||||
|     transform: perspective(400px) rotate3d(1, 0, 0, -90deg); | ||||
|     transition-timing-function: ease-in; | ||||
| @@ -121,11 +117,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-bouncyflip.ns-hide { | ||||
|   animation-name: flipInXSimple; | ||||
|   animation-name: flip-in-x-simple; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| @keyframes flipInXSimple { | ||||
| @keyframes flip-in-x-simple { | ||||
|   0% { | ||||
|     transform: perspective(400px) rotate3d(1, 0, 0, -90deg); | ||||
|     transition-timing-function: ease-in; | ||||
| @@ -145,11 +141,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-show { | ||||
|   animation-name: animLoad; | ||||
|   animation-name: anim-load; | ||||
|   animation-duration: 1s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animLoad { | ||||
| @keyframes anim-load { | ||||
|   0% { | ||||
|     opacity: 1; | ||||
|     transform: scale3d(0, 0.3, 1); | ||||
| @@ -162,7 +158,7 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-hide { | ||||
|   animation-name: animFade; | ||||
|   animation-name: anim-fade; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| @@ -174,15 +170,15 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-show .ns-close { | ||||
|   animation-name: animFade; | ||||
|   animation-name: anim-fade; | ||||
| } | ||||
| 
 | ||||
| .ns-effect-exploader.ns-show .ns-box-inner { | ||||
|   animation-name: animFadeMove; | ||||
|   animation-name: anim-fade-move; | ||||
|   animation-timing-function: ease-out; | ||||
| } | ||||
| 
 | ||||
| @keyframes animFadeMove { | ||||
| @keyframes anim-fade-move { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     transform: translate3d(0, 10px, 0); | ||||
| @@ -194,7 +190,7 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes animFade { | ||||
| @keyframes anim-fade { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
| @@ -206,11 +202,11 @@ | ||||
| 
 | ||||
| .ns-effect-scale.ns-show, | ||||
| .ns-effect-scale.ns-hide { | ||||
|   animation-name: animScale; | ||||
|   animation-name: anim-scale; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animScale { | ||||
| @keyframes anim-scale { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1); | ||||
| @@ -223,168 +219,169 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-jelly.ns-show { | ||||
|   animation-name: animJelly; | ||||
|   animation-name: anim-jelly; | ||||
|   animation-duration: 1s; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| 
 | ||||
| .ns-effect-jelly.ns-hide { | ||||
|   animation-name: animFade; | ||||
|   animation-name: anim-fade; | ||||
|   animation-duration: 0.3s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animFade { | ||||
| @keyframes anim-fade { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     opacity: 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes animJelly { | ||||
| @keyframes anim-jelly { | ||||
|   0% { | ||||
|     transform: matrix3d(0.7, 0, 0, 0, 0, 0.7, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   2.083333% { | ||||
|     transform: matrix3d(0.75266, 0, 0, 0, 0, 0.76342, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.7527, 0, 0, 0, 0, 0.7634, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   4.166667% { | ||||
|     transform: matrix3d(0.81071, 0, 0, 0, 0, 0.84545, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.8107, 0, 0, 0, 0, 0.8454, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   6.25% { | ||||
|     transform: matrix3d(0.86808, 0, 0, 0, 0, 0.9286, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.8681, 0, 0, 0, 0, 0.929, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   8.333333% { | ||||
|     transform: matrix3d(0.92038, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   10.416667% { | ||||
|     transform: matrix3d(0.96482, 0, 0, 0, 0, 1.05202, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9648, 0, 0, 0, 0, 1.052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   12.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.08204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.082, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   14.583333% { | ||||
|     transform: matrix3d(1.02563, 0, 0, 0, 0, 1.09149, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0256, 0, 0, 0, 0, 1.0915, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   16.666667% { | ||||
|     transform: matrix3d(1.04227, 0, 0, 0, 0, 1.08453, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0423, 0, 0, 0, 0, 1.0845, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   18.75% { | ||||
|     transform: matrix3d(1.05102, 0, 0, 0, 0, 1.06666, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.051, 0, 0, 0, 0, 1.0667, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   20.833333% { | ||||
|     transform: matrix3d(1.05334, 0, 0, 0, 0, 1.04355, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0533, 0, 0, 0, 0, 1.0436, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   22.916667% { | ||||
|     transform: matrix3d(1.05078, 0, 0, 0, 0, 1.02012, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0508, 0, 0, 0, 0, 1.0201, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   25% { | ||||
|     transform: matrix3d(1.04487, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0449, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   27.083333% { | ||||
|     transform: matrix3d(1.03699, 0, 0, 0, 0, 0.98534, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.037, 0, 0, 0, 0, 0.9853, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   29.166667% { | ||||
|     transform: matrix3d(1.02831, 0, 0, 0, 0, 0.97688, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0283, 0, 0, 0, 0, 0.9769, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   31.25% { | ||||
|     transform: matrix3d(1.01973, 0, 0, 0, 0, 0.97422, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0197, 0, 0, 0, 0, 0.9742, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   33.333333% { | ||||
|     transform: matrix3d(1.01191, 0, 0, 0, 0, 0.97618, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0119, 0, 0, 0, 0, 0.9762, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   35.416667% { | ||||
|     transform: matrix3d(1.00526, 0, 0, 0, 0, 0.98122, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0053, 0, 0, 0, 0, 0.9812, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   37.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.98773, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.9877, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   39.583333% { | ||||
|     transform: matrix3d(0.99617, 0, 0, 0, 0, 0.99433, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9962, 0, 0, 0, 0, 0.9943, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   41.666667% { | ||||
|     transform: matrix3d(0.99368, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9937, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   43.75% { | ||||
|     transform: matrix3d(0.99237, 0, 0, 0, 0, 1.00413, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0041, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   45.833333% { | ||||
|     transform: matrix3d(0.99202, 0, 0, 0, 0, 1.00651, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.992, 0, 0, 0, 0, 1.0065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   47.916667% { | ||||
|     transform: matrix3d(0.99241, 0, 0, 0, 0, 1.00726, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0073, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   50% { | ||||
|     transform: matrix3d(0.99329, 0, 0, 0, 0, 1.00671, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9933, 0, 0, 0, 0, 1.0067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   52.083333% { | ||||
|     transform: matrix3d(0.99447, 0, 0, 0, 0, 1.00529, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9945, 0, 0, 0, 0, 1.0053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   54.166667% { | ||||
|     transform: matrix3d(0.99577, 0, 0, 0, 0, 1.00346, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9958, 0, 0, 0, 0, 1.0035, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   56.25% { | ||||
|     transform: matrix3d(0.99705, 0, 0, 0, 0, 1.0016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.997, 0, 0, 0, 0, 1.002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   58.333333% { | ||||
|     transform: matrix3d(0.99822, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   60.416667% { | ||||
|     transform: matrix3d(0.99921, 0, 0, 0, 0, 0.99884, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9992, 0, 0, 0, 0, 0.9989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   62.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.99816, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   64.583333% { | ||||
|     transform: matrix3d(1.00057, 0, 0, 0, 0, 0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0006, 0, 0, 0, 0, 0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   66.666667% { | ||||
|     transform: matrix3d(1.00095, 0, 0, 0, 0, 0.99811, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.001, 0, 0, 0, 0, 0.9981, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   68.75% { | ||||
|     transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   70.833333% { | ||||
|     transform: matrix3d(1.00119, 0, 0, 0, 0, 0.99903, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0012, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   72.916667% { | ||||
|     transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99955, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   75% { | ||||
| @@ -392,47 +389,47 @@ | ||||
|   } | ||||
| 
 | ||||
|   77.083333% { | ||||
|     transform: matrix3d(1.00083, 0, 0, 0, 0, 1.00033, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0008, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   79.166667% { | ||||
|     transform: matrix3d(1.00063, 0, 0, 0, 0, 1.00052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0006, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   81.25% { | ||||
|     transform: matrix3d(1.00044, 0, 0, 0, 0, 1.00058, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0004, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   83.333333% { | ||||
|     transform: matrix3d(1.00027, 0, 0, 0, 0, 1.00053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0003, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   85.416667% { | ||||
|     transform: matrix3d(1.00012, 0, 0, 0, 0, 1.00042, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1.0001, 0, 0, 0, 0, 1.0004, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   87.5% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.00027, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   89.583333% { | ||||
|     transform: matrix3d(0.99991, 0, 0, 0, 0, 1.00013, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9999, 0, 0, 0, 0, 1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   91.666667% { | ||||
|     transform: matrix3d(0.99986, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   93.75% { | ||||
|     transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   95.833333% { | ||||
|     transform: matrix3d(0.99982, 0, 0, 0, 0, 0.99985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   97.916667% { | ||||
|     transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99984, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|     transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
| @@ -441,162 +438,162 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-left.ns-show { | ||||
|   animation-name: animSlideElasticLeft; | ||||
|   animation-name: anim-slide-elastic-left; | ||||
|   animation-duration: 1s; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideElasticLeft { | ||||
| @keyframes anim-slide-elastic-left { | ||||
|   0% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1000, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   1.666667% { | ||||
|     transform: matrix3d(1.92933, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.26805, 0, 0, 1); | ||||
|     transform: matrix3d(1.9293, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.2681, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   3.333333% { | ||||
|     transform: matrix3d(1.96989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.82545, 0, 0, 1); | ||||
|     transform: matrix3d(1.9699, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.8255, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   5% { | ||||
|     transform: matrix3d(1.70901, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.26115, 0, 0, 1); | ||||
|     transform: matrix3d(1.709, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.2612, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   6.666667% { | ||||
|     transform: matrix3d(1.4235, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.3238, 0, 0, 1); | ||||
|     transform: matrix3d(1.424, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.324, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   8.333333% { | ||||
|     transform: matrix3d(1.21065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.29848, 0, 0, 1); | ||||
|     transform: matrix3d(1.2107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.2985, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   10% { | ||||
|     transform: matrix3d(1.08167, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.59273, 0, 0, 1); | ||||
|     transform: matrix3d(1.0817, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.5927, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   11.666667% { | ||||
|     transform: matrix3d(1.0165, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.72371, 0, 0, 1); | ||||
|     transform: matrix3d(1.017, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.7237, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   13.333333% { | ||||
|     transform: matrix3d(0.99057, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.12794, 0, 0, 1); | ||||
|     transform: matrix3d(0.9906, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.1279, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   15% { | ||||
|     transform: matrix3d(0.98478, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.86339, 0, 0, 1); | ||||
|     transform: matrix3d(0.9848, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.8634, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   16.666667% { | ||||
|     transform: matrix3d(0.98719, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.40503, 0, 0, 1); | ||||
|     transform: matrix3d(0.9872, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.405, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   18.333333% { | ||||
|     transform: matrix3d(0.9916, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.75275, 0, 0, 1); | ||||
|     transform: matrix3d(0.992, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.7528, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   20% { | ||||
|     transform: matrix3d(0.99541, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.10141, 0, 0, 1); | ||||
|     transform: matrix3d(0.9954, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.1014, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   21.666667% { | ||||
|     transform: matrix3d(0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.98271, 0, 0, 1); | ||||
|     transform: matrix3d(0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.9827, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   23.333333% { | ||||
|     transform: matrix3d(0.99936, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.40752, 0, 0, 1); | ||||
|     transform: matrix3d(0.9994, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.4075, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   25% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.99558, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.9956, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   26.666667% { | ||||
|     transform: matrix3d(1.00021, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.08575, 0, 0, 1); | ||||
|     transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.0858, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   28.333333% { | ||||
|     transform: matrix3d(1.00022, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.82507, 0, 0, 1); | ||||
|     transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.8251, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   30% { | ||||
|     transform: matrix3d(1.00016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.23737, 0, 0, 1); | ||||
|     transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.2374, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   31.666667% { | ||||
|     transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.27389, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.2739, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   33.333333% { | ||||
|     transform: matrix3d(1.00005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.84893, 0, 0, 1); | ||||
|     transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.8489, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   35% { | ||||
|     transform: matrix3d(1.00002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.86364, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.8636, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   36.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.22079, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.2208, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   38.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16687, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1669, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   40% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.37284, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3728, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   41.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.45594, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4559, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   43.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.46116, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4612, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   45% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4214, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.421, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   46.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.35963, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3596, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   48.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.29103, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.291, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   50% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.22487, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.2249, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   51.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16624, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1662, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   53.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.11734, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1173, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   55% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.07854, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0785, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   56.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.04909, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0491, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   58.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.02773, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0277, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   60% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.01295, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.013, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   61.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00331, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0033, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   63.333333% { | ||||
| @@ -604,67 +601,67 @@ | ||||
|   } | ||||
| 
 | ||||
|   65% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00559, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0056, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   66.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00684, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0068, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   68.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00692, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0069, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   70% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00632, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0063, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   71.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00539, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0054, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   73.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00436, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0044, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   75% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00337, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0034, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   76.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00249, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0025, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   78.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00176, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0018, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   80% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00118, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0012, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   81.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00074, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0007, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   83.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00042, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0004, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   85% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00019, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0002, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   86.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00005, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   88.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00004, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   90% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   91.666667% { | ||||
| @@ -676,15 +673,15 @@ | ||||
|   } | ||||
| 
 | ||||
|   95% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00009, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   96.666667% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   98.333333% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00007, 0, 0, 1); | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
| @@ -693,11 +690,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-left.ns-hide { | ||||
|   animation-name: animSlideLeft; | ||||
|   animation-name: anim-slide-left; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideLeft { | ||||
| @keyframes anim-slide-left { | ||||
|   0% { | ||||
|     transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0); | ||||
|   } | ||||
| @@ -708,10 +705,10 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-right.ns-show { | ||||
|   animation: animSlideElasticRight 2000ms linear both; | ||||
|   animation: anim-slide-elastic-right 2000ms linear both; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideElasticRight { | ||||
| @keyframes anim-slide-elastic-right { | ||||
|   0% { | ||||
|     transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1); | ||||
|   } | ||||
| @@ -790,11 +787,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-right.ns-hide { | ||||
|   animation-name: animSlideRight; | ||||
|   animation-name: anim-slide-right; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideRight { | ||||
| @keyframes anim-slide-right { | ||||
|   0% { | ||||
|     transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0); | ||||
|   } | ||||
| @@ -805,10 +802,10 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-center.ns-show { | ||||
|   animation: animSlideElasticCenter 2000ms linear both; | ||||
|   animation: anim-slide-elastic-center 2000ms linear both; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideElasticCenter { | ||||
| @keyframes anim-slide-elastic-center { | ||||
|   0% { | ||||
|     transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1); | ||||
|   } | ||||
| @@ -887,11 +884,11 @@ | ||||
| } | ||||
| 
 | ||||
| .ns-effect-slide-center.ns-hide { | ||||
|   animation-name: animSlideCenter; | ||||
|   animation-name: anim-slide-center; | ||||
|   animation-duration: 0.25s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animSlideCenter { | ||||
| @keyframes anim-slide-center { | ||||
|   0% { | ||||
|     transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0); | ||||
|   } | ||||
| @@ -903,11 +900,11 @@ | ||||
| 
 | ||||
| .ns-effect-genie.ns-show, | ||||
| .ns-effect-genie.ns-hide { | ||||
|   animation-name: animGenie; | ||||
|   animation-name: anim-genie; | ||||
|   animation-duration: 0.4s; | ||||
| } | ||||
| 
 | ||||
| @keyframes animGenie { | ||||
| @keyframes anim-genie { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1); | ||||
							
								
								
									
										4
									
								
								modules/default/alert/styles/right.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/styles/right.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| .ns-box { | ||||
|   margin-left: auto; | ||||
|   text-align: right; | ||||
| } | ||||
							
								
								
									
										20
									
								
								modules/default/alert/templates/alert.njk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								modules/default/alert/templates/alert.njk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| {% if imageUrl or imageFA %} | ||||
|     {% set imageHeight = imageHeight if imageHeight else "80px" %} | ||||
|     {% if imageUrl %} | ||||
|         <img src="{{ imageUrl }}" | ||||
|              height="{{ imageHeight }}" | ||||
|              style="margin-bottom: 10px" /> | ||||
|     {% else %} | ||||
|         <span class="bright fas fa-{{ imageFA }}" | ||||
|               style="margin-bottom: 10px; | ||||
|                      font-size: {{ imageHeight }}"></span> | ||||
|     {% endif %} | ||||
|     <br /> | ||||
| {% endif %} | ||||
| {% if title %} | ||||
|     <span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span> | ||||
| {% endif %} | ||||
| {% if message %} | ||||
|     {% if title %}<br />{% endif %} | ||||
|     <span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span> | ||||
| {% endif %} | ||||
							
								
								
									
										7
									
								
								modules/default/alert/templates/notification.njk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								modules/default/alert/templates/notification.njk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| {% if title %} | ||||
|     <span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span> | ||||
| {% endif %} | ||||
| {% if message %} | ||||
|     {% if title %}<br />{% endif %} | ||||
|     <span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span> | ||||
| {% endif %} | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror нотификация", | ||||
| 	"sysTitle": "MagicMirror² нотификация", | ||||
| 	"welcome": "Добре дошли, стартирането беше успешно" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notifikation", | ||||
| 	"sysTitle": "MagicMirror² Notifikation", | ||||
| 	"welcome": "Velkommen, modulet er succesfuldt startet!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Benachrichtigung", | ||||
| 	"sysTitle": "MagicMirror² Benachrichtigung", | ||||
| 	"welcome": "Willkommen, Start war erfolgreich!" | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								modules/default/alert/translations/el.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/translations/el.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror² Ειδοποίηση", | ||||
| 	"welcome": "Καλώς ήρθατε, η εκκίνηση ήταν επιτυχής!" | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notification", | ||||
| 	"sysTitle": "MagicMirror² Notification", | ||||
| 	"welcome": "Welcome, start was successful!" | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								modules/default/alert/translations/eo.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/translations/eo.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror²-sciigo", | ||||
| 	"welcome": "Bonvenon, lanĉo sukcesis!" | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notificaciones", | ||||
| 	"sysTitle": "MagicMirror² Notificaciones", | ||||
| 	"welcome": "Bienvenido, ¡se iniciado correctamente!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notification", | ||||
| 	"sysTitle": "MagicMirror² Notification", | ||||
| 	"welcome": "Bienvenue, le démarrage a été un succès!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror értesítés", | ||||
| 	"sysTitle": "MagicMirror² értesítés", | ||||
| 	"welcome": "Üdvözöljük, indulás sikeres!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Notificatie", | ||||
| 	"sysTitle": "MagicMirror² Notificatie", | ||||
| 	"welcome": "Welkom, Succesvol gestart!" | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "MagicMirror Уведомление", | ||||
| 	"sysTitle": "MagicMirror² Уведомление", | ||||
| 	"welcome": "Добро пожаловать, старт был успешным!" | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								modules/default/alert/translations/th.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/default/alert/translations/th.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
| 	"sysTitle": "การแจ้งเตือน MagicMirror²", | ||||
| 	"welcome": "ยินดีต้อนรับ การเริ่มต้นสำเร็จแล้ว!" | ||||
| } | ||||
							
								
								
									
										2
									
								
								modules/default/calendar/README.md
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										2
									
								
								modules/default/calendar/README.md
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,6 +1,6 @@ | ||||
| # Module: Calendar | ||||
|  | ||||
| The `calendar` module is one of the default modules of the MagicMirror. | ||||
| The `calendar` module is one of the default modules of the MagicMirror². | ||||
| This module displays events from a public .ical calendar. It can combine multiple calendars. | ||||
|  | ||||
| For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/calendar.html). | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
| .calendar .title { | ||||
|   padding-left: 0; | ||||
|   padding-right: 0; | ||||
|   vertical-align: top; | ||||
| } | ||||
|  | ||||
| .calendar .time { | ||||
|   | ||||
							
								
								
									
										767
									
								
								modules/default/calendar/calendar.js
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										767
									
								
								modules/default/calendar/calendar.js
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,16 +1,8 @@ | ||||
| /* Magic Mirror | ||||
|  * Node Helper: Calendar - CalendarFetcher | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
| const CalendarUtils = require("./calendarutils"); | ||||
| const https = require("node:https"); | ||||
| const ical = require("node-ical"); | ||||
| const Log = require("logger"); | ||||
| const NodeHelper = require("node_helper"); | ||||
| const ical = require("node-ical"); | ||||
| const fetch = require("node-fetch"); | ||||
| const digest = require("digest-fetch"); | ||||
| const https = require("https"); | ||||
| const CalendarFetcherUtils = require("./calendarfetcherutils"); | ||||
|  | ||||
| /** | ||||
|  * | ||||
| @@ -38,10 +30,9 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
| 		clearTimeout(reloadTimer); | ||||
| 		reloadTimer = null; | ||||
| 		const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); | ||||
| 		let fetcher = null; | ||||
| 		let httpsAgent = null; | ||||
| 		let headers = { | ||||
| 			"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)" | ||||
| 			"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}` | ||||
| 		}; | ||||
|  | ||||
| 		if (selfSignedCert) { | ||||
| @@ -51,18 +42,13 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
| 		} | ||||
| 		if (auth) { | ||||
| 			if (auth.method === "bearer") { | ||||
| 				headers.Authorization = "Bearer " + auth.pass; | ||||
| 			} else if (auth.method === "digest") { | ||||
| 				fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent }); | ||||
| 				headers.Authorization = `Bearer ${auth.pass}`; | ||||
| 			} else { | ||||
| 				headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64"); | ||||
| 				headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`; | ||||
| 			} | ||||
| 		} | ||||
| 		if (fetcher === null) { | ||||
| 			fetcher = fetch(url, { headers: headers, agent: httpsAgent }); | ||||
| 		} | ||||
|  | ||||
| 		fetcher | ||||
| 		fetch(url, { headers: headers, agent: httpsAgent }) | ||||
| 			.then(NodeHelper.checkFetchStatus) | ||||
| 			.then((response) => response.text()) | ||||
| 			.then((responseData) => { | ||||
| @@ -70,8 +56,8 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
|  | ||||
| 				try { | ||||
| 					data = ical.parseICS(responseData); | ||||
| 					Log.debug("parsed data=" + JSON.stringify(data)); | ||||
| 					events = CalendarUtils.filterEvents(data, { | ||||
| 					Log.debug(`parsed data=${JSON.stringify(data, null, 2)}`); | ||||
| 					events = CalendarFetcherUtils.filterEvents(data, { | ||||
| 						excludedEvents, | ||||
| 						includePastEvents, | ||||
| 						maximumEntries, | ||||
| @@ -95,10 +81,13 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
| 	 * Schedule the timer for the next update. | ||||
| 	 */ | ||||
| 	const scheduleTimer = function () { | ||||
| 		clearTimeout(reloadTimer); | ||||
| 		reloadTimer = setTimeout(function () { | ||||
| 			fetchCalendar(); | ||||
| 		}, reloadInterval); | ||||
| 		if (process.env.JEST_WORKER_ID === undefined) { | ||||
| 			// only set timer when not running in jest | ||||
| 			clearTimeout(reloadTimer); | ||||
| 			reloadTimer = setTimeout(function () { | ||||
| 				fetchCalendar(); | ||||
| 			}, reloadInterval); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	/* public methods */ | ||||
| @@ -114,13 +103,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
| 	 * Broadcast the existing events. | ||||
| 	 */ | ||||
| 	this.broadcastEvents = function () { | ||||
| 		Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events."); | ||||
| 		Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`); | ||||
| 		eventsReceivedCallback(this); | ||||
| 	}; | ||||
|  | ||||
| 	/** | ||||
| 	 * Sets the on success callback | ||||
| 	 * | ||||
| 	 * @param {Function} callback The on success callback. | ||||
| 	 */ | ||||
| 	this.onReceive = function (callback) { | ||||
| @@ -129,7 +117,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
|  | ||||
| 	/** | ||||
| 	 * Sets the on error callback | ||||
| 	 * | ||||
| 	 * @param {Function} callback The on error callback. | ||||
| 	 */ | ||||
| 	this.onError = function (callback) { | ||||
| @@ -138,7 +125,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns the url of this fetcher. | ||||
| 	 * | ||||
| 	 * @returns {string} The url of this fetcher. | ||||
| 	 */ | ||||
| 	this.url = function () { | ||||
| @@ -147,7 +133,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn | ||||
|  | ||||
| 	/** | ||||
| 	 * Returns current available events for this fetcher. | ||||
| 	 * | ||||
| 	 * @returns {object[]} The current available events for this fetcher. | ||||
| 	 */ | ||||
| 	this.events = function () { | ||||
|   | ||||
							
								
								
									
										431
									
								
								modules/default/calendar/calendarfetcherutils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										431
									
								
								modules/default/calendar/calendarfetcherutils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,431 @@ | ||||
| /** | ||||
|  * @external Moment | ||||
|  */ | ||||
| const moment = require("moment-timezone"); | ||||
|  | ||||
| const Log = require("../../../js/logger"); | ||||
|  | ||||
| const CalendarFetcherUtils = { | ||||
|  | ||||
| 	/** | ||||
| 	 * Determine based on the title of an event if it should be excluded from the list of events | ||||
| 	 * TODO This seems like an overly complicated way to exclude events based on the title. | ||||
| 	 * @param {object} config the global config | ||||
| 	 * @param {string} title the title of the event | ||||
| 	 * @returns {object} excluded: true if the event should be excluded, false otherwise | ||||
| 	 * until: the date until the event should be excluded. | ||||
| 	 */ | ||||
| 	shouldEventBeExcluded (config, title) { | ||||
| 		let filter = { | ||||
| 			excluded: false, | ||||
| 			until: null | ||||
| 		}; | ||||
| 		for (let f in config.excludedEvents) { | ||||
| 			let filter = config.excludedEvents[f], | ||||
| 				testTitle = title.toLowerCase(), | ||||
| 				until = null, | ||||
| 				useRegex = false, | ||||
| 				regexFlags = "g"; | ||||
|  | ||||
| 			if (filter instanceof Object) { | ||||
| 				if (typeof filter.until !== "undefined") { | ||||
| 					until = filter.until; | ||||
| 				} | ||||
|  | ||||
| 				if (typeof filter.regex !== "undefined") { | ||||
| 					useRegex = filter.regex; | ||||
| 				} | ||||
|  | ||||
| 				// If additional advanced filtering is added in, this section | ||||
| 				// must remain last as we overwrite the filter object with the | ||||
| 				// filterBy string | ||||
| 				if (filter.caseSensitive) { | ||||
| 					filter = filter.filterBy; | ||||
| 					testTitle = title; | ||||
| 				} else if (useRegex) { | ||||
| 					filter = filter.filterBy; | ||||
| 					testTitle = title; | ||||
| 					regexFlags += "i"; | ||||
| 				} else { | ||||
| 					filter = filter.filterBy.toLowerCase(); | ||||
| 				} | ||||
| 			} else { | ||||
| 				filter = filter.toLowerCase(); | ||||
| 			} | ||||
|  | ||||
| 			if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) { | ||||
| 				if (until) { | ||||
| 					filter.until = until; | ||||
| 				} else { | ||||
| 					filter.excluded = true; | ||||
| 				} | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| 		return filter; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Get local timezone. | ||||
| 	 * This method makes it easier to test if different timezones cause problems by changing this implementation. | ||||
| 	 * @returns {string} timezone | ||||
| 	 */ | ||||
| 	getLocalTimezone () { | ||||
| 		return moment.tz.guess(); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * This function returns a list of moments for a recurring event. | ||||
| 	 * @param {object} event the current event which is a recurring event | ||||
| 	 * @param {moment.Moment} pastLocalMoment The past date to search for recurring events | ||||
| 	 * @param {moment.Moment} futureLocalMoment The future date to search for recurring events | ||||
| 	 * @param {number} durationInMs the duration of the event, this is used to take into account currently running events | ||||
| 	 * @returns {moment.Moment[]} All moments for the recurring event | ||||
| 	 */ | ||||
| 	getMomentsFromRecurringEvent (event, pastLocalMoment, futureLocalMoment, durationInMs) { | ||||
| 		const rule = event.rrule; | ||||
|  | ||||
| 		// can cause problems with e.g. birthdays before 1900 | ||||
| 		if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) { | ||||
| 			rule.origOptions.dtstart.setYear(1900); | ||||
| 			rule.options.dtstart.setYear(1900); | ||||
| 		} | ||||
|  | ||||
| 		// subtract the max of the duration of this event or 1 day to find events in the past that are currently still running and should therefor be displayed. | ||||
| 		const oneDayInMs = 24 * 60 * 60000; | ||||
| 		let searchFromDate = pastLocalMoment.clone().subtract(Math.max(durationInMs, oneDayInMs), "milliseconds").toDate(); | ||||
| 		let searchToDate = futureLocalMoment.clone().add(1, "days").toDate(); | ||||
| 		Log.debug(`Search for recurring events between: ${searchFromDate} and ${searchToDate}`); | ||||
|  | ||||
| 		// if until is set, and its a full day event, force the time to midnight. rrule gets confused with non-00 offset | ||||
| 		// looks like MS Outlook sets the until time incorrectly for fullday events | ||||
| 		if ((rule.options.until !== undefined) && CalendarFetcherUtils.isFullDayEvent(event)) { | ||||
| 			Log.debug("fixup rrule until"); | ||||
| 			rule.options.until = moment(rule.options.until).clone().startOf("day").add(1, "day") | ||||
| 				.toDate(); | ||||
| 		} | ||||
|  | ||||
| 		Log.debug("fix rrule start=", rule.options.dtstart); | ||||
| 		Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate); | ||||
|  | ||||
| 		Log.debug(`RRule: ${rule.toString()}`); | ||||
| 		rule.options.tzid = null; // RRule gets *very* confused with timezones | ||||
|  | ||||
| 		let dates = rule.between(searchFromDate, searchToDate, true, () => { | ||||
| 			return true; | ||||
| 		}); | ||||
|  | ||||
| 		Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`); | ||||
|  | ||||
| 		// shouldn't need this  anymore, as RRULE not passed junk | ||||
| 		dates = dates.filter((d) => { | ||||
| 			return JSON.stringify(d) !== "null"; | ||||
| 		}); | ||||
|  | ||||
| 		// Dates are returned in UTC timezone but with localdatetime because tzid is null. | ||||
| 		// So we map the date to a moment using the original timezone of the event. | ||||
| 		return dates.map((d) => (event.start.tz ? moment.tz(d, "UTC").tz(event.start.tz, true) : moment.tz(d, "UTC").tz(CalendarFetcherUtils.getLocalTimezone(), true))); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Filter the events from ical according to the given config | ||||
| 	 * @param {object} data the calendar data from ical | ||||
| 	 * @param {object} config The configuration object | ||||
| 	 * @returns {string[]} the filtered events | ||||
| 	 */ | ||||
| 	filterEvents (data, config) { | ||||
| 		const newEvents = []; | ||||
|  | ||||
| 		const eventDate = function (event, time) { | ||||
| 			const startMoment = event[time].tz ? moment.tz(event[time], event[time].tz) : moment.tz(event[time], CalendarFetcherUtils.getLocalTimezone()); | ||||
| 			return CalendarFetcherUtils.isFullDayEvent(event) ? startMoment.startOf("day") : startMoment; | ||||
| 		}; | ||||
|  | ||||
| 		Log.debug(`There are ${Object.entries(data).length} calendar entries.`); | ||||
|  | ||||
| 		const now = moment(); | ||||
| 		const pastLocalMoment = config.includePastEvents ? now.clone().startOf("day").subtract(config.maximumNumberOfDays, "days") : now; | ||||
| 		const futureLocalMoment | ||||
| 			= now | ||||
| 				.clone() | ||||
| 				.startOf("day") | ||||
| 				.add(config.maximumNumberOfDays, "days") | ||||
| 				// Subtract 1 second so that events that start on the middle of the night will not repeat. | ||||
| 				.subtract(1, "seconds"); | ||||
|  | ||||
| 		Object.entries(data).forEach(([key, event]) => { | ||||
| 			Log.debug("Processing entry..."); | ||||
|  | ||||
| 			const title = CalendarFetcherUtils.getTitleFromEvent(event); | ||||
| 			Log.debug(`title: ${title}`); | ||||
|  | ||||
| 			// Return quickly if event should be excluded. | ||||
| 			let { excluded, eventFilterUntil } = this.shouldEventBeExcluded(config, title); | ||||
| 			if (excluded) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			// FIXME: Ugly fix to solve the facebook birthday issue. | ||||
| 			// Otherwise, the recurring events only show the birthday for next year. | ||||
| 			let isFacebookBirthday = false; | ||||
| 			if (typeof event.uid !== "undefined") { | ||||
| 				if (event.uid.indexOf("@facebook.com") !== -1) { | ||||
| 					isFacebookBirthday = true; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if (event.type === "VEVENT") { | ||||
| 				Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`); | ||||
| 				let eventStartMoment = eventDate(event, "start"); | ||||
| 				let eventEndMoment; | ||||
|  | ||||
| 				if (typeof event.end !== "undefined") { | ||||
| 					eventEndMoment = eventDate(event, "end"); | ||||
| 				} else if (typeof event.duration !== "undefined") { | ||||
| 					eventEndMoment = eventStartMoment.clone().add(moment.duration(event.duration)); | ||||
| 				} else { | ||||
| 					if (!isFacebookBirthday) { | ||||
| 						// make copy of start date, separate storage area | ||||
| 						eventEndMoment = eventStartMoment.clone(); | ||||
| 					} else { | ||||
| 						eventEndMoment = eventStartMoment.clone().add(1, "days"); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				Log.debug(`start: ${eventStartMoment.toDate()}`); | ||||
| 				Log.debug(`end:: ${eventEndMoment.toDate()}`); | ||||
|  | ||||
| 				// Calculate the duration of the event for use with recurring events. | ||||
| 				const durationMs = eventEndMoment.valueOf() - eventStartMoment.valueOf(); | ||||
| 				Log.debug(`duration: ${durationMs}`); | ||||
|  | ||||
| 				const location = event.location || false; | ||||
| 				const geo = event.geo || false; | ||||
| 				const description = event.description || false; | ||||
|  | ||||
| 				// TODO This should be a seperate function. | ||||
| 				if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) { | ||||
| 					// Recurring event. | ||||
| 					let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs); | ||||
|  | ||||
| 					// Loop through the set of moment entries to see which recurrences should be added to our event list. | ||||
| 					// TODO This should create an event per moment so we can change anything we want. | ||||
| 					for (let m in moments) { | ||||
| 						let curEvent = event; | ||||
| 						let showRecurrence = true; | ||||
| 						let recurringEventStartMoment = moments[m].tz(CalendarFetcherUtils.getLocalTimezone()).clone(); | ||||
| 						let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms"); | ||||
|  | ||||
| 						let dateKey = recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD"); | ||||
|  | ||||
| 						Log.debug("event date dateKey=", dateKey); | ||||
| 						// For each date that we're checking, it's possible that there is a recurrence override for that one day. | ||||
| 						if (curEvent.recurrences !== undefined) { | ||||
| 							Log.debug("have recurrences=", curEvent.recurrences); | ||||
| 							if (curEvent.recurrences[dateKey] !== undefined) { | ||||
| 								Log.debug("have a recurrence match for dateKey=", dateKey); | ||||
| 								// We found an override, so for this recurrence, use a potentially different title, start date, and duration. | ||||
| 								curEvent = curEvent.recurrences[dateKey]; | ||||
| 								// Some event start/end dates don't have timezones | ||||
| 								if (curEvent.start.tz) { | ||||
| 									recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz).tz(CalendarFetcherUtils.getLocalTimezone()); | ||||
| 								} else { | ||||
| 									recurringEventStartMoment = moment(curEvent.start).tz(CalendarFetcherUtils.getLocalTimezone()); | ||||
| 								} | ||||
| 								if (curEvent.end.tz) { | ||||
| 									recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz).tz(CalendarFetcherUtils.getLocalTimezone()); | ||||
| 								} else { | ||||
| 									recurringEventEndMoment = moment(curEvent.end).tz(CalendarFetcherUtils.getLocalTimezone()); | ||||
| 								} | ||||
| 							} else { | ||||
| 								Log.debug("recurrence key ", dateKey, " doesn't match"); | ||||
| 							} | ||||
| 						} | ||||
| 						// If there's no recurrence override, check for an exception date.  Exception dates represent exceptions to the rule. | ||||
| 						if (curEvent.exdate !== undefined) { | ||||
| 							Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate); | ||||
| 							if (curEvent.exdate[dateKey] !== undefined) { | ||||
| 								// This date is an exception date, which means we should skip it in the recurrence pattern. | ||||
| 								showRecurrence = false; | ||||
| 							} | ||||
| 						} | ||||
|  | ||||
| 						if (recurringEventStartMoment.valueOf() === recurringEventEndMoment.valueOf()) { | ||||
| 							recurringEventEndMoment = recurringEventEndMoment.endOf("day"); | ||||
| 						} | ||||
|  | ||||
| 						const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent); | ||||
|  | ||||
| 						// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add | ||||
| 						// it to the event list. | ||||
| 						if (recurringEventEndMoment.isBefore(pastLocalMoment) || recurringEventStartMoment.isAfter(futureLocalMoment)) { | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
|  | ||||
| 						if (CalendarFetcherUtils.timeFilterApplies(now, recurringEventEndMoment, eventFilterUntil)) { | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
|  | ||||
| 						if (showRecurrence === true) { | ||||
| 							Log.debug(`saving event: ${recurrenceTitle}`); | ||||
| 							newEvents.push({ | ||||
| 								title: recurrenceTitle, | ||||
| 								startDate: recurringEventStartMoment.format("x"), | ||||
| 								endDate: recurringEventEndMoment.format("x"), | ||||
| 								fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event), | ||||
| 								recurringEvent: true, | ||||
| 								class: event.class, | ||||
| 								firstYear: event.start.getFullYear(), | ||||
| 								location: location, | ||||
| 								geo: geo, | ||||
| 								description: description | ||||
| 							}); | ||||
| 						} else { | ||||
| 							Log.debug("not saving event ", recurrenceTitle, eventStartMoment); | ||||
| 						} | ||||
| 						Log.debug(" "); | ||||
| 					} | ||||
| 					// End recurring event parsing. | ||||
| 				} else { | ||||
| 					// Single event. | ||||
| 					const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event); | ||||
| 					// Log.debug("full day event") | ||||
|  | ||||
| 					// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00) | ||||
| 					if (fullDayEvent && eventStartMoment.valueOf() === eventEndMoment.valueOf()) { | ||||
| 						eventEndMoment = eventEndMoment.endOf("day"); | ||||
| 					} | ||||
|  | ||||
| 					if (config.includePastEvents) { | ||||
| 						// Past event is too far in the past, so skip. | ||||
| 						if (eventEndMoment < pastLocalMoment) { | ||||
| 							return; | ||||
| 						} | ||||
| 					} else { | ||||
| 						// It's not a fullday event, and it is in the past, so skip. | ||||
| 						if (!fullDayEvent && eventEndMoment < now) { | ||||
| 							return; | ||||
| 						} | ||||
|  | ||||
| 						// It's a fullday event, and it is before today, So skip. | ||||
| 						if (fullDayEvent && eventEndMoment <= now.startOf("day")) { | ||||
| 							return; | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					// It exceeds the maximumNumberOfDays limit, so skip. | ||||
| 					if (eventStartMoment > futureLocalMoment) { | ||||
| 						return; | ||||
| 					} | ||||
|  | ||||
| 					if (CalendarFetcherUtils.timeFilterApplies(now, eventEndMoment, eventFilterUntil)) { | ||||
| 						return; | ||||
| 					} | ||||
|  | ||||
| 					// Every thing is good. Add it to the list. | ||||
| 					newEvents.push({ | ||||
| 						title: title, | ||||
| 						startDate: eventStartMoment.format("x"), | ||||
| 						endDate: eventEndMoment.format("x"), | ||||
| 						fullDayEvent: fullDayEvent, | ||||
| 						recurringEvent: false, | ||||
| 						class: event.class, | ||||
| 						firstYear: event.start.getFullYear(), | ||||
| 						location: location, | ||||
| 						geo: geo, | ||||
| 						description: description | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		newEvents.sort(function (a, b) { | ||||
| 			return a.startDate - b.startDate; | ||||
| 		}); | ||||
|  | ||||
| 		return newEvents; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Gets the title from the event. | ||||
| 	 * @param {object} event The event object to check. | ||||
| 	 * @returns {string} The title of the event, or "Event" if no title is found. | ||||
| 	 */ | ||||
| 	getTitleFromEvent (event) { | ||||
| 		let title = "Event"; | ||||
| 		if (event.summary) { | ||||
| 			title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary; | ||||
| 		} else if (event.description) { | ||||
| 			title = event.description; | ||||
| 		} | ||||
|  | ||||
| 		return title; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks if an event is a fullday event. | ||||
| 	 * @param {object} event The event object to check. | ||||
| 	 * @returns {boolean} True if the event is a fullday event, false otherwise | ||||
| 	 */ | ||||
| 	isFullDayEvent (event) { | ||||
| 		if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		const start = event.start || 0; | ||||
| 		const startDate = new Date(start); | ||||
| 		const end = event.end || 0; | ||||
| 		if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) { | ||||
| 			// Is 24 hours, and starts on the middle of the night. | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Determines if the user defined time filter should apply | ||||
| 	 * @param {moment.Moment} now Date object using previously created object for consistency | ||||
| 	 * @param {moment.Moment} endDate Moment object representing the event end date | ||||
| 	 * @param {string} filter The time to subtract from the end date to determine if an event should be shown | ||||
| 	 * @returns {boolean} True if the event should be filtered out, false otherwise | ||||
| 	 */ | ||||
| 	timeFilterApplies (now, endDate, filter) { | ||||
| 		if (filter) { | ||||
| 			const until = filter.split(" "), | ||||
| 				value = parseInt(until[0]), | ||||
| 				increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js | ||||
| 				filterUntil = moment(endDate.format()).subtract(value, increment); | ||||
|  | ||||
| 			return now < filterUntil; | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Determines if the user defined title filter should apply | ||||
| 	 * @param {string} title the title of the event | ||||
| 	 * @param {string} filter the string to look for, can be a regex also | ||||
| 	 * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string | ||||
| 	 * @param {string} regexFlags flags that should be applied to the regex | ||||
| 	 * @returns {boolean} True if the title should be filtered out, false otherwise | ||||
| 	 */ | ||||
| 	titleFilterApplies (title, filter, useRegex, regexFlags) { | ||||
| 		if (useRegex) { | ||||
| 			let regexFilter = filter; | ||||
| 			// Assume if leading slash, there is also trailing slash | ||||
| 			if (filter[0] === "/") { | ||||
| 				// Strip leading and trailing slashes | ||||
| 				regexFilter = filter.substr(1).slice(0, -1); | ||||
| 			} | ||||
| 			return new RegExp(regexFilter, regexFlags).test(title); | ||||
| 		} else { | ||||
| 			return title.includes(filter); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| if (typeof module !== "undefined") { | ||||
| 	module.exports = CalendarFetcherUtils; | ||||
| } | ||||
| @@ -1,605 +1,125 @@ | ||||
| /* Magic Mirror | ||||
|  * Calendar Util Methods | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @external Moment | ||||
|  */ | ||||
| const moment = require("moment"); | ||||
| const path = require("path"); | ||||
| const zoneTable = require(path.join(__dirname, "windowsZones.json")); | ||||
| const Log = require("../../../js/logger.js"); | ||||
|  | ||||
| const CalendarUtils = { | ||||
|  | ||||
| 	/** | ||||
| 	 * Calculate the time correction, either dst/std or full day in cases where | ||||
| 	 * utc time is day before plus offset | ||||
| 	 * | ||||
| 	 * @param {object} event the event which needs adjustement | ||||
| 	 * @param {Date} date the date on which this event happens | ||||
| 	 * @returns {number} the necessary adjustment in hours | ||||
| 	 * Capitalize the first letter of a string | ||||
| 	 * @param {string} string The string to capitalize | ||||
| 	 * @returns {string} The capitalized string | ||||
| 	 */ | ||||
| 	calculateTimezoneAdjustment: function (event, date) { | ||||
| 		let adjustHours = 0; | ||||
| 		// if a timezone was specified | ||||
| 		if (!event.start.tz) { | ||||
| 			Log.debug(" if no tz, guess based on now"); | ||||
| 			event.start.tz = moment.tz.guess(); | ||||
| 		} | ||||
| 		Log.debug("initial tz=" + event.start.tz); | ||||
|  | ||||
| 		// if there is a start date specified | ||||
| 		if (event.start.tz) { | ||||
| 			// if this is a windows timezone | ||||
| 			if (event.start.tz.includes(" ")) { | ||||
| 				// use the lookup table to get theIANA name as moment and date don't know MS timezones | ||||
| 				let tz = CalendarUtils.getIanaTZFromMS(event.start.tz); | ||||
| 				Log.debug("corrected TZ=" + tz); | ||||
| 				// watch out for unregistered windows timezone names | ||||
| 				// if we had a successful lookup | ||||
| 				if (tz) { | ||||
| 					// change the timezone to the IANA name | ||||
| 					event.start.tz = tz; | ||||
| 					// Log.debug("corrected timezone="+event.start.tz) | ||||
| 				} | ||||
| 			} | ||||
| 			Log.debug("corrected tz=" + event.start.tz); | ||||
| 			let current_offset = 0; // offset  from TZ string or calculated | ||||
| 			let mm = 0; // date with tz or offset | ||||
| 			let start_offset = 0; // utc offset of created with tz | ||||
| 			// if there is still an offset, lookup failed, use it | ||||
| 			if (event.start.tz.startsWith("(")) { | ||||
| 				const regex = /[+|-]\d*:\d*/; | ||||
| 				const start_offsetString = event.start.tz.match(regex).toString().split(":"); | ||||
| 				let start_offset = parseInt(start_offsetString[0]); | ||||
| 				start_offset *= event.start.tz[1] === "-" ? -1 : 1; | ||||
| 				adjustHours = start_offset; | ||||
| 				Log.debug("defined offset=" + start_offset + " hours"); | ||||
| 				current_offset = start_offset; | ||||
| 				event.start.tz = ""; | ||||
| 				Log.debug("ical offset=" + current_offset + " date=" + date); | ||||
| 				mm = moment(date); | ||||
| 				let x = parseInt(moment(new Date()).utcOffset()); | ||||
| 				Log.debug("net mins=" + (current_offset * 60 - x)); | ||||
|  | ||||
| 				mm = mm.add(x - current_offset * 60, "minutes"); | ||||
| 				adjustHours = (current_offset * 60 - x) / 60; | ||||
| 				event.start = mm.toDate(); | ||||
| 				Log.debug("adjusted date=" + event.start); | ||||
| 			} else { | ||||
| 				// get the start time in that timezone | ||||
| 				let es = moment(event.start); | ||||
| 				// check for start date prior to start of daylight changing date | ||||
| 				if (es.format("YYYY") < 2007) { | ||||
| 					es.set("year", 2013); // if so, use a closer date | ||||
| 				} | ||||
| 				Log.debug("start date/time=" + es.toDate()); | ||||
| 				start_offset = moment.tz(es, event.start.tz).utcOffset(); | ||||
| 				Log.debug("start offset=" + start_offset); | ||||
|  | ||||
| 				Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate()); | ||||
|  | ||||
| 				// get the specified date in that timezone | ||||
| 				mm = moment.tz(moment(date), event.start.tz); | ||||
| 				Log.debug("event date=" + mm.toDate()); | ||||
| 				current_offset = mm.utcOffset(); | ||||
| 			} | ||||
| 			Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate()); | ||||
|  | ||||
| 			// if the offset is greater than 0, east of london | ||||
| 			if (current_offset !== start_offset) { | ||||
| 				// big offset | ||||
| 				Log.debug("offset"); | ||||
| 				let h = parseInt(mm.format("H")); | ||||
| 				// check if the event time is less than the offset | ||||
| 				if (h > 0 && h < Math.abs(current_offset) / 60) { | ||||
| 					// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time) | ||||
| 					// we need to fix that | ||||
| 					adjustHours = 24; | ||||
| 					// Log.debug("adjusting date") | ||||
| 				} | ||||
| 				//-300 > -240 | ||||
| 				//if (Math.abs(current_offset) > Math.abs(start_offset)){ | ||||
| 				if (current_offset > start_offset) { | ||||
| 					adjustHours -= 1; | ||||
| 					Log.debug("adjust down 1 hour dst change"); | ||||
| 					//} else if (Math.abs(current_offset) < Math.abs(start_offset)) { | ||||
| 				} else if (current_offset < start_offset) { | ||||
| 					adjustHours += 1; | ||||
| 					Log.debug("adjust up 1 hour dst change"); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		Log.debug("adjustHours=" + adjustHours); | ||||
| 		return adjustHours; | ||||
| 	capFirst (string) { | ||||
| 		return string.charAt(0).toUpperCase() + string.slice(1); | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Filter the events from ical according to the given config | ||||
| 	 * | ||||
| 	 * @param {object} data the calendar data from ical | ||||
| 	 * @param {object} config The configuration object | ||||
| 	 * @returns {string[]} the filtered events | ||||
| 	 * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the | ||||
| 	 * corresponding time-format to be used in the calendar display. If no number is given (or otherwise invalid input) | ||||
| 	 * it will a localeSpecification object with the system locale time format. | ||||
| 	 * @param {number} timeFormat Specifies either 12 or 24-hour time format | ||||
| 	 * @returns {moment.LocaleSpecification} formatted time | ||||
| 	 */ | ||||
| 	filterEvents: function (data, config) { | ||||
| 		const newEvents = []; | ||||
|  | ||||
| 		// limitFunction doesn't do much limiting, see comment re: the dates | ||||
| 		// array in rrule section below as to why we need to do the filtering | ||||
| 		// ourselves | ||||
| 		const limitFunction = function (date, i) { | ||||
| 			return true; | ||||
| 		}; | ||||
|  | ||||
| 		const eventDate = function (event, time) { | ||||
| 			return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time])); | ||||
| 		}; | ||||
|  | ||||
| 		Log.debug("there are " + Object.entries(data).length + " calendar entries"); | ||||
| 		Object.entries(data).forEach(([key, event]) => { | ||||
| 			const now = new Date(); | ||||
| 			const today = moment().startOf("day").toDate(); | ||||
| 			const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat. | ||||
| 			let past = today; | ||||
| 			Log.debug("have entries "); | ||||
| 			if (config.includePastEvents) { | ||||
| 				past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate(); | ||||
| 	getLocaleSpecification (timeFormat) { | ||||
| 		switch (timeFormat) { | ||||
| 			case 12: { | ||||
| 				return { longDateFormat: { LT: "h:mm A" } }; | ||||
| 			} | ||||
|  | ||||
| 			// FIXME: Ugly fix to solve the facebook birthday issue. | ||||
| 			// Otherwise, the recurring events only show the birthday for next year. | ||||
| 			let isFacebookBirthday = false; | ||||
| 			if (typeof event.uid !== "undefined") { | ||||
| 				if (event.uid.indexOf("@facebook.com") !== -1) { | ||||
| 					isFacebookBirthday = true; | ||||
| 				} | ||||
| 			case 24: { | ||||
| 				return { longDateFormat: { LT: "HH:mm" } }; | ||||
| 			} | ||||
| 			default: { | ||||
| 				return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } }; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 			if (event.type === "VEVENT") { | ||||
| 				let startDate = eventDate(event, "start"); | ||||
| 				let endDate; | ||||
| 	/** | ||||
| 	 * Shortens a string if it's longer than maxLength and add an ellipsis to the end | ||||
| 	 * @param {string} string Text string to shorten | ||||
| 	 * @param {number} maxLength The max length of the string | ||||
| 	 * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength | ||||
| 	 * @param {number} maxTitleLines The max number of vertical lines before cutting event title | ||||
| 	 * @returns {string} The shortened string | ||||
| 	 */ | ||||
| 	shorten (string, maxLength, wrapEvents, maxTitleLines) { | ||||
| 		if (typeof string !== "string") { | ||||
| 			return ""; | ||||
| 		} | ||||
|  | ||||
| 				Log.debug("\nevent=" + JSON.stringify(event)); | ||||
| 				if (typeof event.end !== "undefined") { | ||||
| 					endDate = eventDate(event, "end"); | ||||
| 				} else if (typeof event.duration !== "undefined") { | ||||
| 					endDate = startDate.clone().add(moment.duration(event.duration)); | ||||
| 		if (wrapEvents === true) { | ||||
| 			const words = string.split(" "); | ||||
| 			let temp = ""; | ||||
| 			let currentLine = ""; | ||||
| 			let line = 0; | ||||
|  | ||||
| 			for (let i = 0; i < words.length; i++) { | ||||
| 				const word = words[i]; | ||||
| 				if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { | ||||
| 					// max - 1 to account for a space | ||||
| 					currentLine += `${word} `; | ||||
| 				} else { | ||||
| 					if (!isFacebookBirthday) { | ||||
| 						// make copy of start date, separate storage area | ||||
| 						endDate = moment(startDate.format("x"), "x"); | ||||
| 					} else { | ||||
| 						endDate = moment(startDate).add(1, "days"); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate()); | ||||
|  | ||||
| 				// calculate the duration of the event for use with recurring events. | ||||
| 				let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); | ||||
|  | ||||
| 				if (event.start.length === 8) { | ||||
| 					startDate = startDate.startOf("day"); | ||||
| 				} | ||||
|  | ||||
| 				const title = CalendarUtils.getTitleFromEvent(event); | ||||
|  | ||||
| 				let excluded = false, | ||||
| 					dateFilter = null; | ||||
|  | ||||
| 				for (let f in config.excludedEvents) { | ||||
| 					let filter = config.excludedEvents[f], | ||||
| 						testTitle = title.toLowerCase(), | ||||
| 						until = null, | ||||
| 						useRegex = false, | ||||
| 						regexFlags = "g"; | ||||
|  | ||||
| 					if (filter instanceof Object) { | ||||
| 						if (typeof filter.until !== "undefined") { | ||||
| 							until = filter.until; | ||||
| 						} | ||||
|  | ||||
| 						if (typeof filter.regex !== "undefined") { | ||||
| 							useRegex = filter.regex; | ||||
| 						} | ||||
|  | ||||
| 						// If additional advanced filtering is added in, this section | ||||
| 						// must remain last as we overwrite the filter object with the | ||||
| 						// filterBy string | ||||
| 						if (filter.caseSensitive) { | ||||
| 							filter = filter.filterBy; | ||||
| 							testTitle = title; | ||||
| 						} else if (useRegex) { | ||||
| 							filter = filter.filterBy; | ||||
| 							testTitle = title; | ||||
| 							regexFlags += "i"; | ||||
| 						} else { | ||||
| 							filter = filter.filterBy.toLowerCase(); | ||||
| 						} | ||||
| 					} else { | ||||
| 						filter = filter.toLowerCase(); | ||||
| 					} | ||||
|  | ||||
| 					if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) { | ||||
| 						if (until) { | ||||
| 							dateFilter = until; | ||||
| 						} else { | ||||
| 							excluded = true; | ||||
| 					line++; | ||||
| 					if (line > maxTitleLines - 1) { | ||||
| 						if (i < words.length) { | ||||
| 							currentLine += "…"; | ||||
| 						} | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if (excluded) { | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				const location = event.location || false; | ||||
| 				const geo = event.geo || false; | ||||
| 				const description = event.description || false; | ||||
|  | ||||
| 				if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) { | ||||
| 					const rule = event.rrule; | ||||
| 					let addedEvents = 0; | ||||
|  | ||||
| 					const pastMoment = moment(past); | ||||
| 					const futureMoment = moment(future); | ||||
|  | ||||
| 					// can cause problems with e.g. birthdays before 1900 | ||||
| 					if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) { | ||||
| 						rule.origOptions.dtstart.setYear(1900); | ||||
| 						rule.options.dtstart.setYear(1900); | ||||
| 					} | ||||
|  | ||||
| 					// For recurring events, get the set of start dates that fall within the range | ||||
| 					// of dates we're looking for. | ||||
| 					// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time | ||||
| 					let pastLocal = 0; | ||||
| 					let futureLocal = 0; | ||||
| 					if (CalendarUtils.isFullDayEvent(event)) { | ||||
| 						// if full day event, only use the date part of the ranges | ||||
| 						pastLocal = pastMoment.toDate(); | ||||
| 						futureLocal = futureMoment.toDate(); | ||||
| 					if (currentLine.length > 0) { | ||||
| 						temp += `${currentLine}<br>${word} `; | ||||
| 					} else { | ||||
| 						// if we want past events | ||||
| 						if (config.includePastEvents) { | ||||
| 							// use the calculated past time for the between from | ||||
| 							pastLocal = pastMoment.toDate(); | ||||
| 						} else { | ||||
| 							// otherwise use NOW.. cause we shouldn't use any before now | ||||
| 							pastLocal = moment().toDate(); //now | ||||
| 						} | ||||
| 						futureLocal = futureMoment.toDate(); // future | ||||
| 						temp += `${word}<br>`; | ||||
| 					} | ||||
| 					Log.debug(" between=" + pastLocal + " to " + futureLocal); | ||||
| 					const dates = rule.between(pastLocal, futureLocal, true, limitFunction); | ||||
| 					Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates)); | ||||
| 					// The "dates" array contains the set of dates within our desired date range range that are valid | ||||
| 					// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that | ||||
| 					// had its date changed from outside the range to inside the range.  For the time being, | ||||
| 					// we'll handle this by adding *all* recurrence entries into the set of dates that we check, | ||||
| 					// because the logic below will filter out any recurrences that don't actually belong within | ||||
| 					// our display range. | ||||
| 					// Would be great if there was a better way to handle this. | ||||
| 					if (event.recurrences !== undefined) { | ||||
| 						for (let r in event.recurrences) { | ||||
| 							// Only add dates that weren't already in the range we added from the rrule so that | ||||
| 							// we don"t double-add those events. | ||||
| 							if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) { | ||||
| 								dates.push(new Date(r)); | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 					// Loop through the set of date entries to see which recurrences should be added to our event list. | ||||
| 					for (let d in dates) { | ||||
| 						let date = dates[d]; | ||||
| 						// ical.js started returning recurrences and exdates as ISOStrings without time information. | ||||
| 						// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same | ||||
| 						// (see https://github.com/peterbraden/ical.js/pull/84 ) | ||||
| 						const dateKey = date.toISOString().substring(0, 10); | ||||
| 						let curEvent = event; | ||||
| 						let showRecurrence = true; | ||||
|  | ||||
| 						// get the offset of today where we are processing | ||||
| 						// this will be the correction we need to apply | ||||
| 						let nowOffset = new Date().getTimezoneOffset(); | ||||
| 						// for full day events, the time might be off from RRULE/Luxon problem | ||||
| 						// get time zone offset of the rule calculated event | ||||
| 						let dateoffset = date.getTimezoneOffset(); | ||||
| 						// reduce the time by the offset | ||||
| 						Log.debug(" recurring date is " + date + " offset is " + dateoffset); | ||||
| 						let dh = moment(date).format("HH"); | ||||
| 						Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh); | ||||
| 						if (CalendarUtils.isFullDayEvent(event)) { | ||||
| 							Log.debug("fullday"); | ||||
| 							// if the offset is  negative, east of GMT where the problem is | ||||
| 							if (dateoffset < 0) { | ||||
| 								// if the date hour is less than the offset | ||||
| 								if (dh < Math.abs(dateoffset / 60)) { | ||||
| 									// reduce the time by the offset | ||||
| 									Log.debug(" recurring date is " + date + " offset is " + dateoffset); | ||||
| 									// apply the correction to the date/time to get it UTC relative | ||||
| 									date = new Date(date.getTime() - Math.abs(nowOffset) * 60000); | ||||
| 									// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 									// fix it for this event entry | ||||
| 									//duration = 24 * 60 * 60 * 1000; | ||||
| 									Log.debug("new recurring date1 is " + date); | ||||
| 								} | ||||
| 							} else { | ||||
| 								// if the timezones are the same, correct date if needed | ||||
| 								if (event.start.tz === moment.tz.guess()) { | ||||
| 									// if the date hour is less than the offset | ||||
| 									if (24 - dh < Math.abs(dateoffset / 60)) { | ||||
| 										// apply the correction to the date/time back to right day | ||||
| 										date = new Date(date.getTime() + Math.abs(24 * 60) * 60000); | ||||
| 										// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 										// fix it for this event entry | ||||
| 										//duration = 24 * 60 * 60 * 1000; | ||||
| 										Log.debug("new recurring date2 is " + date); | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} else { | ||||
| 							// not full day, but luxon can still screw up the date on the rule processing | ||||
| 							// we need to correct the date to get back to the right event for | ||||
| 							if (dateoffset < 0) { | ||||
| 								// if the date hour is less than the offset | ||||
| 								if (dh < Math.abs(dateoffset / 60)) { | ||||
| 									// reduce the time by the offset | ||||
| 									Log.debug(" recurring date is " + date + " offset is " + dateoffset); | ||||
| 									// apply the correction to the date/time to get it UTC relative | ||||
| 									date = new Date(date.getTime() - Math.abs(nowOffset) * 60000); | ||||
| 									// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 									// fix it for this event entry | ||||
| 									//duration = 24 * 60 * 60 * 1000; | ||||
| 									Log.debug("new recurring date1 is " + date); | ||||
| 								} | ||||
| 							} else { | ||||
| 								// if the timezones are the same, correct date if needed | ||||
| 								if (event.start.tz === moment.tz.guess()) { | ||||
| 									// if the date hour is less than the offset | ||||
| 									if (24 - dh < Math.abs(dateoffset / 60)) { | ||||
| 										// apply the correction to the date/time back to right day | ||||
| 										date = new Date(date.getTime() + Math.abs(24 * 60) * 60000); | ||||
| 										// the duration was calculated way back at the top before we could correct the start time.. | ||||
| 										// fix it for this event entry | ||||
| 										//duration = 24 * 60 * 60 * 1000; | ||||
| 										Log.debug("new recurring date2 is " + date); | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 						startDate = moment(date); | ||||
|  | ||||
| 						let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date); | ||||
|  | ||||
| 						// For each date that we're checking, it's possible that there is a recurrence override for that one day. | ||||
| 						if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) { | ||||
| 							// We found an override, so for this recurrence, use a potentially different title, start date, and duration. | ||||
| 							curEvent = curEvent.recurrences[dateKey]; | ||||
| 							startDate = moment(curEvent.start); | ||||
| 							duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x")); | ||||
| 						} | ||||
| 						// If there's no recurrence override, check for an exception date.  Exception dates represent exceptions to the rule. | ||||
| 						else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) { | ||||
| 							// This date is an exception date, which means we should skip it in the recurrence pattern. | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
| 						Log.debug("duration=" + duration); | ||||
|  | ||||
| 						endDate = moment(parseInt(startDate.format("x")) + duration, "x"); | ||||
| 						if (startDate.format("x") === endDate.format("x")) { | ||||
| 							endDate = endDate.endOf("day"); | ||||
| 						} | ||||
|  | ||||
| 						const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent); | ||||
|  | ||||
| 						// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add | ||||
| 						// it to the event list. | ||||
| 						if (endDate.isBefore(past) || startDate.isAfter(future)) { | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
|  | ||||
| 						if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) { | ||||
| 							showRecurrence = false; | ||||
| 						} | ||||
|  | ||||
| 						if (showRecurrence === true) { | ||||
| 							Log.debug("saving event =" + description); | ||||
| 							addedEvents++; | ||||
| 							newEvents.push({ | ||||
| 								title: recurrenceTitle, | ||||
| 								startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), | ||||
| 								endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), | ||||
| 								fullDayEvent: CalendarUtils.isFullDayEvent(event), | ||||
| 								recurringEvent: true, | ||||
| 								class: event.class, | ||||
| 								firstYear: event.start.getFullYear(), | ||||
| 								location: location, | ||||
| 								geo: geo, | ||||
| 								description: description | ||||
| 							}); | ||||
| 						} | ||||
| 					} | ||||
| 					// end recurring event parsing | ||||
| 				} else { | ||||
| 					// Single event. | ||||
| 					const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event); | ||||
| 					// Log.debug("full day event") | ||||
|  | ||||
| 					if (config.includePastEvents) { | ||||
| 						// Past event is too far in the past, so skip. | ||||
| 						if (endDate < past) { | ||||
| 							return; | ||||
| 						} | ||||
| 					} else { | ||||
| 						// It's not a fullday event, and it is in the past, so skip. | ||||
| 						if (!fullDayEvent && endDate < new Date()) { | ||||
| 							return; | ||||
| 						} | ||||
|  | ||||
| 						// It's a fullday event, and it is before today, So skip. | ||||
| 						if (fullDayEvent && endDate <= today) { | ||||
| 							return; | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					// It exceeds the maximumNumberOfDays limit, so skip. | ||||
| 					if (startDate > future) { | ||||
| 						return; | ||||
| 					} | ||||
|  | ||||
| 					if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) { | ||||
| 						return; | ||||
| 					} | ||||
|  | ||||
| 					// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already | ||||
| 					if (fullDayEvent && startDate <= today) { | ||||
| 						startDate = moment(today); | ||||
| 					} | ||||
| 					// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00) | ||||
| 					if (fullDayEvent && startDate.format("x") === endDate.format("x")) { | ||||
| 						endDate = endDate.endOf("day"); | ||||
| 					} | ||||
| 					// get correction for date saving and dst change between now and then | ||||
| 					let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate()); | ||||
| 					// Every thing is good. Add it to the list. | ||||
| 					newEvents.push({ | ||||
| 						title: title, | ||||
| 						startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"), | ||||
| 						endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"), | ||||
| 						fullDayEvent: fullDayEvent, | ||||
| 						class: event.class, | ||||
| 						location: location, | ||||
| 						geo: geo, | ||||
| 						description: description | ||||
| 					}); | ||||
| 					currentLine = ""; | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		newEvents.sort(function (a, b) { | ||||
| 			return a.startDate - b.startDate; | ||||
| 		}); | ||||
|  | ||||
| 		// include up to maximumEntries current or upcoming events | ||||
| 		// If past events should be included, include all past events | ||||
| 		const now = moment(); | ||||
| 		let entries = 0; | ||||
| 		let events = []; | ||||
| 		for (let ne of newEvents) { | ||||
| 			if (moment(ne.endDate, "x").isBefore(now)) { | ||||
| 				if (config.includePastEvents) events.push(ne); | ||||
| 				continue; | ||||
| 			} | ||||
| 			entries++; | ||||
| 			// If max events has been saved, skip the rest | ||||
| 			if (entries > config.maximumEntries) break; | ||||
| 			events.push(ne); | ||||
| 		} | ||||
|  | ||||
| 		return events; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Lookup iana tz from windows | ||||
| 	 * | ||||
| 	 * @param {string} msTZName the timezone name to lookup | ||||
| 	 * @returns {string|null} the iana name or null of none is found | ||||
| 	 */ | ||||
| 	getIanaTZFromMS: function (msTZName) { | ||||
| 		// Get hash entry | ||||
| 		const he = zoneTable[msTZName]; | ||||
| 		// If found return iana name, else null | ||||
| 		return he ? he.iana[0] : null; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Gets the title from the event. | ||||
| 	 * | ||||
| 	 * @param {object} event The event object to check. | ||||
| 	 * @returns {string} The title of the event, or "Event" if no title is found. | ||||
| 	 */ | ||||
| 	getTitleFromEvent: function (event) { | ||||
| 		let title = "Event"; | ||||
| 		if (event.summary) { | ||||
| 			title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary; | ||||
| 		} else if (event.description) { | ||||
| 			title = event.description; | ||||
| 		} | ||||
|  | ||||
| 		return title; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks if an event is a fullday event. | ||||
| 	 * | ||||
| 	 * @param {object} event The event object to check. | ||||
| 	 * @returns {boolean} True if the event is a fullday event, false otherwise | ||||
| 	 */ | ||||
| 	isFullDayEvent: function (event) { | ||||
| 		if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") { | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		const start = event.start || 0; | ||||
| 		const startDate = new Date(start); | ||||
| 		const end = event.end || 0; | ||||
| 		if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) { | ||||
| 			// Is 24 hours, and starts on the middle of the night. | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Determines if the user defined time filter should apply | ||||
| 	 * | ||||
| 	 * @param {Date} now Date object using previously created object for consistency | ||||
| 	 * @param {Moment} endDate Moment object representing the event end date | ||||
| 	 * @param {string} filter The time to subtract from the end date to determine if an event should be shown | ||||
| 	 * @returns {boolean} True if the event should be filtered out, false otherwise | ||||
| 	 */ | ||||
| 	timeFilterApplies: function (now, endDate, filter) { | ||||
| 		if (filter) { | ||||
| 			const until = filter.split(" "), | ||||
| 				value = parseInt(until[0]), | ||||
| 				increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js | ||||
| 				filterUntil = moment(endDate.format()).subtract(value, increment); | ||||
|  | ||||
| 			return now < filterUntil.format("x"); | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Determines if the user defined title filter should apply | ||||
| 	 * | ||||
| 	 * @param {string} title the title of the event | ||||
| 	 * @param {string} filter the string to look for, can be a regex also | ||||
| 	 * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string | ||||
| 	 * @param {string} regexFlags flags that should be applied to the regex | ||||
| 	 * @returns {boolean} True if the title should be filtered out, false otherwise | ||||
| 	 */ | ||||
| 	titleFilterApplies: function (title, filter, useRegex, regexFlags) { | ||||
| 		if (useRegex) { | ||||
| 			// Assume if leading slash, there is also trailing slash | ||||
| 			if (filter[0] === "/") { | ||||
| 				// Strip leading and trailing slashes | ||||
| 				filter = filter.substr(1).slice(0, -1); | ||||
| 			} | ||||
|  | ||||
| 			filter = new RegExp(filter, regexFlags); | ||||
|  | ||||
| 			return filter.test(title); | ||||
| 			return (temp + currentLine).trim(); | ||||
| 		} else { | ||||
| 			return title.includes(filter); | ||||
| 			if (maxLength && typeof maxLength === "number" && string.length > maxLength) { | ||||
| 				return `${string.trim().slice(0, maxLength)}…`; | ||||
| 			} else { | ||||
| 				return string.trim(); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	/** | ||||
| 	 * Transforms the title of an event for usage. | ||||
| 	 * Replaces parts of the text as defined in config.titleReplace. | ||||
| 	 * @param {string} title The title to transform. | ||||
| 	 * @param {object} titleReplace object definition of parts to be replaced in the title | ||||
| 	 *                 object definition: | ||||
| 	 *                    search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calcluation, the element matching the year must be in a RegEx group | ||||
| 	 *                    replace: {string,required} Replacement string, may contain match group references (latter is required for year calculation) | ||||
| 	 *                    yearmatchgroup: {number,optional} match group for year element | ||||
| 	 * @returns {string} The transformed title. | ||||
| 	 */ | ||||
| 	titleTransform (title, titleReplace) { | ||||
| 		let transformedTitle = title; | ||||
| 		for (let tr in titleReplace) { | ||||
| 			let transform = titleReplace[tr]; | ||||
| 			if (typeof transform === "object") { | ||||
| 				if (typeof transform.search !== "undefined" && transform.search !== "" && typeof transform.replace !== "undefined") { | ||||
| 					let regParts = transform.search.match(/^\/(.+)\/([gim]*)$/); | ||||
| 					let needle = new RegExp(transform.search, "g"); | ||||
| 					if (regParts) { | ||||
| 						// the parsed pattern is a regexp with flags. | ||||
| 						needle = new RegExp(regParts[1], regParts[2]); | ||||
| 					} | ||||
|  | ||||
| 					let replacement = transform.replace; | ||||
| 					if (typeof transform.yearmatchgroup !== "undefined" && transform.yearmatchgroup !== "") { | ||||
| 						const yearmatch = [...title.matchAll(needle)]; | ||||
| 						if (yearmatch[0].length >= transform.yearmatchgroup + 1 && yearmatch[0][transform.yearmatchgroup] * 1 >= 1900) { | ||||
| 							let calcage = new Date().getFullYear() - yearmatch[0][transform.yearmatchgroup] * 1; | ||||
| 							let searchstr = `$${transform.yearmatchgroup}`; | ||||
| 							replacement = replacement.replace(searchstr, calcage); | ||||
| 						} | ||||
| 					} | ||||
| 					transformedTitle = transformedTitle.replace(needle, replacement); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return transformedTitle; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| /* CalendarFetcher Tester | ||||
| /* | ||||
|  * CalendarFetcher Tester | ||||
|  * use this script with `node debug.js` to test the fetcher without the need | ||||
|  * of starting the MagicMirror core. Adjust the values below to your desire. | ||||
|  * | ||||
|  * By Michael Teeuw https://michaelteeuw.nl | ||||
|  * MIT Licensed. | ||||
|  * of starting the MagicMirror² core. Adjust the values below to your desire. | ||||
|  */ | ||||
| // Alias modules mentioned in package.js under _moduleAliases. | ||||
| require("module-alias/register"); | ||||
|  | ||||
| const CalendarFetcher = require("./calendarfetcher.js"); | ||||
| const CalendarFetcher = require("./calendarfetcher"); | ||||
|  | ||||
| const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL | ||||
| //const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured  in Google OAuth2 first) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user