Compare commits
2027 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4267208f28 | |||
| 25d6ec157f | |||
| b2fc7df06d | |||
| c875371db9 | |||
| dd7818b960 | |||
| 0dd6446f3e | |||
| 579dc4959b | |||
| 27c3170c80 | |||
| 7b275008b5 | |||
| 2cfea229fd | |||
| e349193745 | |||
| 45d68c4da8 | |||
| 36d443fa36 | |||
| 8549663ed0 | |||
| 73b2feae59 | |||
| 0871b416a7 | |||
| 767bfba670 | |||
| 019cbeb617 | |||
| a7d8a8fd1f | |||
| f7e073c857 | |||
| 2fdb3668e2 | |||
| f1aee1d9a4 | |||
| 057f75ca5f | |||
| 314654f319 | |||
| 961959d361 | |||
| d603ea50e2 | |||
| 3b7bc6beba | |||
| e437168e87 | |||
| cf7d735c56 | |||
| b2a41cc4d5 | |||
| 5bc3cb6353 | |||
| e6024ac85b | |||
| 4f6fac2336 | |||
| 12feb68bf0 | |||
| d5d325bf4e | |||
| 47ae1443d1 | |||
| 63b51d2fca | |||
| 3547be3401 | |||
| a038028c3b | |||
| 40cc12e568 | |||
| 4a1a915add | |||
| 3096b67b76 | |||
| 3acceca32f | |||
| e8e05159d8 | |||
| ae0c7390f2 | |||
| 11f2acd702 | |||
| 1759e257c4 | |||
| 3e86c59607 | |||
| dfe5b6b1f2 | |||
| b0026eafb5 | |||
| 67519fb203 | |||
| 7f4a9d6016 | |||
| 317f6e77d4 | |||
| 5fcfd32f6c | |||
| 0fd056dff9 | |||
| 10cf8f1d0e | |||
| 67d95cda76 | |||
| 3ac66f9dd4 | |||
| 43e426ab9f | |||
| a5224258c3 | |||
| ab3a2911c3 | |||
| 1cdcab6047 | |||
| 2ad8ed5550 | |||
| 9226baa63c | |||
| b790d085bb | |||
| 933d5c9139 | |||
| 2c5fd7effa | |||
| f5a5bebae6 | |||
| f2316ec84e | |||
| 84165e5342 | |||
| f65dab114e | |||
| 4ad7504be6 | |||
| e9318d7f11 | |||
| 01d7d41c17 | |||
| f1fb5f2530 | |||
| ea28c10a39 | |||
| 6c84681f35 | |||
| edb3aea880 | |||
| f898925bc0 | |||
| f3be7aa763 | |||
| 71c52a87ec | |||
| 8c786f38ab | |||
| 6bc5826c86 | |||
| 90e36c4552 | |||
| f83bd2e3b7 | |||
| 367eb1b1e1 | |||
| 91e21d69db | |||
| 5bcfea161f | |||
| a9e0ee81ce | |||
| c37c513067 | |||
| c0972b3e14 | |||
| 1b91ae1ab3 | |||
| 4a462597fd | |||
| 505c71855e | |||
| 2ba0d716d1 | |||
| 35ab03c392 | |||
| ddb7551b92 | |||
| fdcc5d68a2 | |||
| ac7c32ad4c | |||
| e93898d2ec | |||
| 22fe27da9c | |||
| e31164140c | |||
| 7d53eeb7f5 | |||
| d3731ff339 | |||
| 02ba2fe59b | |||
| 55091cfe8d | |||
| f6e14a5420 | |||
| b38371400c | |||
| 0458fee326 | |||
| 15256ebbdf | |||
| 773435fb7f | |||
| 1a674a30ac | |||
| 491617e41c | |||
| 24fc480f7b | |||
| 677f11ba1a | |||
| aa7e307205 | |||
| 6ac914904e | |||
| 68dd1fbedb | |||
| 6869380500 | |||
| 3a0febe760 | |||
| 46d195557b | |||
| 548418ecfe | |||
| 28c0d63f89 | |||
| b3eb6ccde3 | |||
| 0e09df797b | |||
| 7fae0751b9 | |||
| e6cdd1ed7a | |||
| 9d8c079d37 | |||
| 9bb58b1649 | |||
| d732adf34b | |||
| 8718e7efd2 | |||
| b21d29098b | |||
| d010330b58 | |||
| 4562024e72 | |||
| 34043d5c97 | |||
| a468b6ff39 | |||
| 534aa7423e | |||
| be416fd335 | |||
| 43962c4a5a | |||
| 1b33b2c48c | |||
| efa4c09306 | |||
| 181b9cdee6 | |||
| a1c3cd272b | |||
| c67b4a4e51 | |||
| 72c97ba224 | |||
| ca96e40397 | |||
| 758daee0c9 | |||
| 37b6a2568d | |||
| 4ee2fd8b14 | |||
| 7706be3e2f | |||
| 16c2316183 | |||
| ce22100b02 | |||
| d2cc38c8ec | |||
| c0a31ccb55 | |||
| 8ddc9e3138 | |||
| 7a24c23bdd | |||
| 32f4b72d68 | |||
| 282df5c2e6 | |||
| 6713ba3798 | |||
| 355edda058 | |||
| b1141f8cbb | |||
| eaad527e32 | |||
| 810922de5e | |||
| da3414e3bc | |||
| 99a0c78fe4 | |||
| 444d3eeb7c | |||
| a3c00c5f75 | |||
| dfb465ef77 | |||
| b20107ad2a | |||
| 6977556984 | |||
| 676f25962d | |||
| 02fe971f0b | |||
| 0b310f849a | |||
| c9c00d2b9f | |||
| 910087e37a | |||
| 9d99794242 | |||
| 3218d7c64d | |||
| 3364e5c876 | |||
| a6355e1945 | |||
| a3a8e67ce2 | |||
| ac388e1daf | |||
| 625a55ddeb | |||
| 6c1b07a7f1 | |||
| b511b0c39a | |||
| 5b43de944d | |||
| e255f4158f | |||
| e0740f89d9 | |||
| a4605238aa | |||
| a4e680ee01 | |||
| edf7d80678 | |||
| 92e6e99bb0 | |||
| 73ed60059f | |||
| 78d8e0b5aa | |||
| 832eca2d9c | |||
| f69bdd1ac4 | |||
| 01f10baa18 | |||
| 23178b24c6 | |||
| 295aafddf0 | |||
| 398b978a5f | |||
| f59999f35f | |||
| e078bf2d2f | |||
| bab1da1ea5 | |||
| 593c9f133d | |||
| 8071eb2d93 | |||
| f44ff12a5f | |||
| 70953333d2 | |||
| e453be1c04 | |||
| 6ac2f8d9c3 | |||
| 06ca87e041 | |||
| 15d1521564 | |||
| 4e1396e7a7 | |||
| ab0f8fc081 | |||
| 0f29652b96 | |||
| 53de61330f | |||
| 2c52190966 | |||
| df66a0ff38 | |||
| 33ced1c16e | |||
| 0b3d3ceb48 | |||
| d385edf348 | |||
| 54cd9c7afa | |||
| 1ccb24018f | |||
| 6bd8e81eae | |||
| edcffd3417 | |||
| 1e599731b8 | |||
| f7c85d8aa2 | |||
| 75e6c8fad6 | |||
| f16170e0b5 | |||
| 74c1e71fe5 | |||
| 471dd08016 | |||
| f8e15acf89 | |||
| 703621d84e | |||
| 0893bf4aa0 | |||
| e44e9f4cf0 | |||
| 6cdbebf0d3 | |||
| 7b85106792 | |||
| cf2a90845b | |||
| 647209cf6d | |||
| 5c0d53b52c | |||
| 56c2b8798c | |||
| d4262e9fdc | |||
| 1902e8206d | |||
| 5918952afb | |||
| 55cd323159 | |||
| 249734f52a | |||
| 8530a62421 | |||
| 65f1260839 | |||
| fc8a679367 | |||
| cafacf4748 | |||
| 5c6ceb2307 | |||
| e05fef18a0 | |||
| 9c8aab6e29 | |||
| 38267a26c1 | |||
| 9e99dae864 | |||
| f9747fd1eb | |||
| bf47368f25 | |||
| 7b82950495 | |||
| 5dd5a20fc1 | |||
| 906e3f8cea | |||
| 44ee3c92b7 | |||
| ae55912176 | |||
| 703b47d916 | |||
| a1816896ca | |||
| e522b4c7f3 | |||
| daa4f7dfb1 | |||
| 39e6463d19 | |||
| f30bc948c3 | |||
| 2b0422b2cb | |||
| 70149846df | |||
| a8f88d7c4d | |||
| 73ddf48d12 | |||
| a9a65fe1f1 | |||
| 70eb5c1053 | |||
| defcf7d220 | |||
| 1f78253b38 | |||
| 40fcdb0fc0 | |||
| 9a3a78c96c | |||
| 7701b41af4 | |||
| 9304d02d78 | |||
| 932b9c0966 | |||
| edb33e1f2c | |||
| fc83306d72 | |||
| 5bdfe23435 | |||
| 503b17acd6 | |||
| ef8c088127 | |||
| 151f15fa15 | |||
| 35db862d12 | |||
| 7948e04ca7 | |||
| b6b9c2a843 | |||
| 95109ef9b0 | |||
| 7fd49be682 | |||
| f16662bcc0 | |||
| 4962597f58 | |||
| 19597bb40b | |||
| 80bc526401 | |||
| 67f1b968bb | |||
| ae0ed07447 | |||
| f5f415e851 | |||
| 8a20c1812f | |||
| 98d4d99c1b | |||
| a38ef2b6f5 | |||
| 5723717576 | |||
| b8f097ff94 | |||
| dcab038020 | |||
| 234c192429 | |||
| 15af5396f8 | |||
| 3fc6da34e8 | |||
| b1dce01b49 | |||
| 885dc48a50 | |||
| 30962c4c7b | |||
| b993cfd294 | |||
| c8c5862b47 | |||
| 0dfd8b9f53 | |||
| 0d3450ceed | |||
| 073ece0527 | |||
| 8020bbd806 | |||
| 1ebd4dbda6 | |||
| beebbf54ad | |||
| f8e5a65f95 | |||
| 379066c782 | |||
| e96ada8c2e | |||
| 0a7d903dd6 | |||
| 0afc671763 | |||
| dd183cbca0 | |||
| cc17373166 | |||
| 19df2e0baa | |||
| 045b4db246 | |||
| 59ccf845b5 | |||
| 814a48c55f | |||
| 4c6cc4abd6 | |||
| 34f47fcfd7 | |||
| 87e64da0d9 | |||
| 730de02874 | |||
| e0fef1da42 | |||
| 3bfa7b1cdd | |||
| fcb59091d2 | |||
| 9146e2a318 | |||
| aefe69c4b5 | |||
| d1ee3d5688 | |||
| f92cdf36f5 | |||
| 8668ff8939 | |||
| 50d37ad752 | |||
| c6afe4fd4e | |||
| 03be4a22d4 | |||
| 85f18f5991 | |||
| 0d237a8f55 | |||
| 7522ac1eaf | |||
| 4704c2a178 | |||
| da818ff577 | |||
| 2660f673db | |||
| fec1a1c0ee | |||
| 537d0203e1 | |||
| 7e00fbfe2d | |||
| b54cfd76dd | |||
| 74783eee0e | |||
| 3994d5bf75 | |||
| 08f7685ca3 | |||
| d08d4962a5 | |||
| 42c9166df2 | |||
| 4d70a0c5bd | |||
| 1202be4686 | |||
| 14f93a91e3 | |||
| 49de124fc0 | |||
| cc939de641 | |||
| fd6c77c73e | |||
| 5c213d31cf | |||
| d8ab1c3a67 | |||
| 5157789774 | |||
| 69d92ba0a8 | |||
| 4686e38a5e | |||
| cab3f60e06 | |||
| f59907d9d7 | |||
| 305441ea28 | |||
| 807094c829 | |||
| cae1b3f47a | |||
| 4c3a8e1fd7 | |||
| 0d5c1b99df | |||
| 656faacc76 | |||
| b8919f8c11 | |||
| b0c042e9dc | |||
| 455116c86f | |||
| 4202452e70 | |||
| 7df7fd635b | |||
| b4fa2d7ce6 | |||
| 8ac3089e0b | |||
| e2f1f3029f | |||
| dd5c438597 | |||
| 60982cc276 | |||
| d23ea29bef | |||
| 5246ea3537 | |||
| 5ad217bd18 | |||
| 5d1c6d35f7 | |||
| 1d49af144c | |||
| 9bdbd49de0 | |||
| e0821d264e | |||
| 5cf94ae35b | |||
| c5fbb73fea | |||
| c30b8942e7 | |||
| e21c1bbc59 | |||
| 87178985f9 | |||
| 0a23332766 | |||
| 9a752ac3a2 | |||
| aaed5a47d7 | |||
| 6e19a7e862 | |||
| 5cd9fe860f | |||
| 498e77c46e | |||
| da69644771 | |||
| 7c0f4ad255 | |||
| 76ec0b6d74 | |||
| c3e604ba49 | |||
| 15848d399c | |||
| 0c791ec52c | |||
| 6938b6e9ac | |||
| a9fe3e1b12 | |||
| c23234d1d8 | |||
| 3855beccce | |||
| 41932c9127 | |||
| 91840904b6 | |||
| 9ba4ac9956 | |||
| 753092db30 | |||
| f0b07f1155 | |||
| 3429060a7b | |||
| 8a6016376c | |||
| 51d0658bdb | |||
| 1cac2f6170 | |||
| 88e209d65b | |||
| 67103e7113 | |||
| 496ada3647 | |||
| 405302e2f0 | |||
| 0530a58530 | |||
| 8ccae822fe | |||
| e45af9b611 | |||
| e8a9bd83d3 | |||
| 8d32c853d5 | |||
| 9137fb2b31 | |||
| d2eeb19f06 | |||
| 43cc91aca2 | |||
| 045187fe2d | |||
| 834e908edd | |||
| 98ba60a51c | |||
| 9bcca0a791 | |||
| 1a588b34fa | |||
| b3398a09ba | |||
| 8fed7034bf | |||
| cfe933c368 | |||
| 79968af8ed | |||
| bd58e02a18 | |||
| d5e82110c3 | |||
| da6be5c490 | |||
| c5b7264f1a | |||
| f8015c156e | |||
| 23012fbb5c | |||
| f3c0e8a835 | |||
| 808428e947 | |||
| 60d3eba712 | |||
| 817b0f8167 | |||
| 116673630a | |||
| 82c8fa2640 | |||
| 14b51c0c74 | |||
| 3c99ccc67b | |||
| c69bc77a7a | |||
| 7ff07804d0 | |||
| 2f4766af9d | |||
| c2e5f19226 | |||
| ac1bd650ce | |||
| c845b63578 | |||
| 4c278a8da5 | |||
| 7b48bc1ef6 | |||
| 2179f199b7 | |||
| 60a3ea0cea | |||
| acaf7cd934 | |||
| 38a2fdff39 | |||
| 14907849cc | |||
| 45ba89c89a | |||
| 9d235fa838 | |||
| 5ea481409f | |||
| 603c2fdc68 | |||
| e384995b63 | |||
| 92444c05aa | |||
| 5831bb49f1 | |||
| ba14d1e846 | |||
| c8e34ac522 | |||
| d41c20f06c | |||
| 30b09856a5 | |||
| 3307c0c183 | |||
| fb1af04b55 | |||
| fda439cb38 | |||
| eb8044e3d4 | |||
| 842177a0aa | |||
| 6913fe1f08 | |||
| aaf2c7f58a | |||
| 8c9ed4907b | |||
| d7c1a8f7ae | |||
| f3855b6548 | |||
| 82096b67a5 | |||
| a560c9c1ee | |||
| 2a089eab08 | |||
| ab4607bf0e | |||
| c9e839f1fd | |||
| 33c75076da | |||
| 8d9fe13490 | |||
| 1752579f9d | |||
| a525104dac | |||
| 375fd7a6aa | |||
| cd79191d7a | |||
| 904994ae24 | |||
| 0a1870d862 | |||
| d1ab79a9e7 | |||
| b37769b935 | |||
| ac7001b96e | |||
| 8c0b88d69a | |||
| 4381809959 | |||
| 54d3cd86b9 | |||
| 1720feeeee | |||
| f7b40d5f92 | |||
| 31df40a841 | |||
| cdda8649fc | |||
| c44863a9bb | |||
| 1e4df539b7 | |||
| 823599fd78 | |||
| 94852460c1 | |||
| 900a789b69 | |||
| 013c181aaa | |||
| 3b38440385 | |||
| 074977c58e | |||
| 3625eb01e2 | |||
| bbc5217c81 | |||
| a5515ad08b | |||
| 12e4441227 | |||
| fdc4ddf316 | |||
| eb2ebead28 | |||
| a8ed7eb914 | |||
| fd509a9099 | |||
| 7bf4b23ee5 | |||
| 937dd2be5e | |||
| 5f9789f99f | |||
| d6d70163d5 | |||
| f4c830e671 | |||
| 950bdb5b76 | |||
| 3068ed75ee | |||
| 42945e1b42 | |||
| faef230d85 | |||
| a7f0dfdff7 | |||
| da28516a98 | |||
| 44e27bf9ab | |||
| 72fdc707ee | |||
| 4d8b6c5ea7 | |||
| fafa299ae1 | |||
| f779957145 | |||
| ce6a26976c | |||
| b53a6da24a | |||
| 438fdaa1b1 | |||
| eb10d0d8d0 | |||
| e1a7056ef8 | |||
| 20fa49f8e4 | |||
| 4ff90029b1 | |||
| 20b75b4065 | |||
| 832a25601d | |||
| 19682ec21b | |||
| b71e2957d3 | |||
| e7fb444bb3 | |||
| fd2705a49d | |||
| 935c9a50ab | |||
| 08c2e989fa | |||
| 942e595444 | |||
| f2225ff6b6 | |||
| 12e073e8cf | |||
| d3b77c3be1 | |||
| 2607ad2e24 | |||
| 67f0801453 | |||
| eebd36ccc8 | |||
| 55d401c098 | |||
| 0c1c5ae9e5 | |||
| d7b5d1f947 | |||
| 80f253e67c | |||
| 39cad02e0d | |||
| 0c79dcdf1b | |||
| cb1bc6cfdf | |||
| a7094de40f | |||
| b5c9f034ca | |||
| d875ed5cf5 | |||
| 53e4b347de | |||
| fafdf88442 | |||
| 0911de205f | |||
| 26fc03ee19 | |||
| f406de3089 | |||
| b62f7a7497 | |||
| 59b428f24b | |||
| 54e8395789 | |||
| 2fabfbe8f6 | |||
| 28ac9e153e | |||
| dadbf1de90 | |||
| 2e16dd983f | |||
| 847f57855c | |||
| 2d412c019c | |||
| 47fc9561ab | |||
| 25fa999259 | |||
| f40145412b | |||
| cec15dbfc0 | |||
| 69d0790484 | |||
| 537a904a49 | |||
| 939643c307 | |||
| 2ce208854f | |||
| 6883b91f8a | |||
| dccdef99c1 | |||
| 76ab99bca6 | |||
| fed0c7a330 | |||
| f179d92c9c | |||
| 907c6dec47 | |||
| 3ea9192f79 | |||
| 6a5cd698a4 | |||
| 179dd0e4d8 | |||
| 4c5763b125 | |||
| 37ed1f750e | |||
| 9528e26487 | |||
| 8143ef1057 | |||
| 2056015fa2 | |||
| 1d98c76c90 | |||
| bddc65e072 | |||
| 1b7a06aaf1 | |||
| fbf12dc764 | |||
| e390a56f05 | |||
| 063c272aea | |||
| c00fd0480c | |||
| ca732f9b8b | |||
| d3a0fe64e7 | |||
| 90256ba68a | |||
| 8b30762f03 | |||
| 77436877ef | |||
| 840e1c5c5b | |||
| ee179a0bd8 | |||
| 2a3ab0ecb2 | |||
| 841c7730e7 | |||
| bf8f8d32a9 | |||
| 8ba56f8b66 | |||
| dc5e7ab98d | |||
| ca7effe0c0 | |||
| 7e543a64e4 | |||
| 98ea5a7b70 | |||
| 7883839871 | |||
| f8d74bbb6d | |||
| 387c399078 | |||
| c184f1e42e | |||
| 5c7d19ce13 | |||
| 3d4b3edc3a | |||
| a7ffc8a172 | |||
| 618d2b993a | |||
| 2458b6b388 | |||
| be933b8b78 | |||
| 38dcb7bd3e | |||
| a001126704 | |||
| fe513b1a07 | |||
| 0825d0511a | |||
| c1ec42a812 | |||
| ae747aa426 | |||
| 70cf212178 | |||
| 5992658164 | |||
| 6e79927bc0 | |||
| 8dffd6181d | |||
| c4ee21bdb0 | |||
| 1d1197e18a | |||
| cc1ff369a7 | |||
| c89030beee | |||
| 4fa65099f9 | |||
| 2a90a0a278 | |||
| 36ab052dda | |||
| fddbe6a574 | |||
| a887be432b | |||
| cf86645bb5 | |||
| 705311f01f | |||
| 81429bfa85 | |||
| 1016d41d7a | |||
| 0ff760fe4a | |||
| f48b1d066e | |||
| 90c82ab1e7 | |||
| 1a0d1f7d79 | |||
| 132132307f | |||
| 2a6e6a671e | |||
| c073f2a15b | |||
| cf1e1aac77 | |||
| 19893601f2 | |||
| 034d35b8a7 | |||
| 7ef9e7eb51 | |||
| 28e23dfdab | |||
| 2c80c4a7d5 | |||
| 5c1147bfa4 | |||
| ee79744735 | |||
| 410d4a47ed | |||
| ead6d9c7d3 | |||
| 4c74908789 | |||
| 547d8ae113 | |||
| 4fcaae8053 | |||
| c935744f4c | |||
| a01e1bad0f | |||
| 3cabbc1328 | |||
| 7192439b2c | |||
| ff6cbf6628 | |||
| 53e9925880 | |||
| 9dfe6242b9 | |||
| ea419509f1 | |||
| a1a683ec56 | |||
| dbe3b6a427 | |||
| e8bd4d05b5 | |||
| 1fb0ed9545 | |||
| ea464bdc7d | |||
| d1c2d0b907 | |||
| 175b2914b6 | |||
| a2c86daef6 | |||
| b40248a1d5 | |||
| ae3a34287a | |||
| b971f2ab22 | |||
| cbc73f5c9a | |||
| 373ef5b7e1 | |||
| 89388940ed | |||
| b2c94adabf | |||
| 3904f50c1b | |||
| d81fa897ad | |||
| a0792aa469 | |||
| 5be03c7ab5 | |||
| fc4da4408c | |||
| 83ab701d02 | |||
| 59aaabecc7 | |||
| 3f07bb5c3a | |||
| 94dd1eb0c2 | |||
| 07ab079715 | |||
| d559df3ee3 | |||
| 69cd766f88 | |||
| 276db17f0c | |||
| cfce39c1de | |||
| ef5631bff4 | |||
| 62017b3ff5 | |||
| 3aafbd2ccb | |||
| 100bea981d | |||
| 931311f11f | |||
| 5e456f378b | |||
| bf315258c5 | |||
| 9780db6fa0 | |||
| 6d0a24cc95 | |||
| 1a3d6e86a8 | |||
| 4f5efef922 | |||
| ff269d414e | |||
| c3e746aa74 | |||
| 511047874e | |||
| 47e40fc762 | |||
| 27c113d8ae | |||
| c082f2a1b1 | |||
| 12f7a3fca0 | |||
| 88d66dee6e | |||
| 3460e6d513 | |||
| 1831caea08 | |||
| b88885582d | |||
| 66650d6dd9 | |||
| 0b65f07960 | |||
| b14e6a7860 | |||
| 68a8c964ea | |||
| f03ac0133f | |||
| 6c1045c545 | |||
| 0cdfd5e62b | |||
| a839638478 | |||
| 1063dbea02 | |||
| c173db69a9 | |||
| bbbab1b2e5 | |||
| c9b6b5b8e1 | |||
| 2386124089 | |||
| 9e907f10b1 | |||
| 2ec6e47086 | |||
| 504e18d9a7 | |||
| b021ff4e21 | |||
| 32c50a9793 | |||
| 44ae48baf5 | |||
| cc3dc1636b | |||
| 545ac689b1 | |||
| e7799fef1c | |||
| d857ca46e0 | |||
| 0e3cc97ee6 | |||
| 66107cf7a4 | |||
| 1e8df5c9d0 | |||
| 4f608bdc5f | |||
| 3da1bae826 | |||
| 48559d3358 | |||
| 5d8871a044 | |||
| a684fadf43 | |||
| 8c858c6b1e | |||
| 7e0803c4b4 | |||
| 697b42c70f | |||
| cb52dfdd0f | |||
| db203a5ad8 | |||
| 3f4df35a3e | |||
| 4661576c8a | |||
| b03bb5a3d7 | |||
| f7d4f9d94e | |||
| c10593e4ac | |||
| 47580c6976 | |||
| 0fa281083e | |||
| 0aca6c2588 | |||
| e181abe797 | |||
| 7818219763 | |||
| 25d20266b8 | |||
| 330505faaf | |||
| f7dd10bc08 | |||
| 20cc82e210 | |||
| 585885e6a2 | |||
| 149af003d4 | |||
| 50a5b9861a | |||
| 17ce6f7291 | |||
| 7047ee9391 | |||
| 5e1c32b606 | |||
| 38032f0b77 | |||
| 6364ea8e57 | |||
| c796e724aa | |||
| 63053f46a8 | |||
| 8bf8c278f0 | |||
| aa0c186c8c | |||
| 5539f74bea | |||
| 11a6cf8236 | |||
| ce63dbde1b | |||
| 989d843fcb | |||
| 32d07e7959 | |||
| 50b585c1dd | |||
| e03beba9bc | |||
| e4ceedcac6 | |||
| 80edfffc3f | |||
| 7a734ebf29 | |||
| 197e159b48 | |||
| 696632e551 | |||
| b1752de36f | |||
| 5967907f86 | |||
| ed288317c4 | |||
| 3754021ae8 | |||
| 7214ce2ede | |||
| d1dcbd97b7 | |||
| 30c0690e90 | |||
| 4b79881472 | |||
| b42be9899e | |||
| 54c2e670e1 | |||
| e35dbba57d | |||
| e632f07708 | |||
| 7f0201b552 | |||
| 70de7ffe69 | |||
| 7a421205e9 | |||
| 3dcc9ad844 | |||
| 5548ed1b57 | |||
| 32bc082a9f | |||
| 4eed77e123 | |||
| 8564105ee3 | |||
| 4a537dde1a | |||
| 2f4d89f32a | |||
| 10d74d3b34 | |||
| e1ba78ff61 | |||
| 31ee456962 | |||
| 717607807a | |||
| 56e4af58d3 | |||
| 622aecfd6d | |||
| be9adb64bb | |||
| bb257f2bf2 | |||
| 76f365f7e8 | |||
| 499336e752 | |||
| e53ec3e43c | |||
| 8b5fe714e1 | |||
| 27c5721e2b | |||
| f3534288f1 | |||
| 87a5829f9e | |||
| 11e9f0eaf2 | |||
| 6f653a603b | |||
| a7bba61b6a | |||
| 15ac81c1cd | |||
| 6b839c8cdc | |||
| 15c25329b6 | |||
| aca85b63ae | |||
| 26637c0f1c | |||
| 56da5493b3 | |||
| abcc9b374c | |||
| 53fc6af1fb | |||
| 93db7ec71e | |||
| f15909c814 | |||
| 7adae569f5 | |||
| a7bf22cafe | |||
| fc9da4756c | |||
| 5c7db14253 | |||
| b42a51cc56 | |||
| 9eee7b2582 | |||
| bf8a5e6a11 | |||
| a7e507a137 | |||
| 04de621e37 | |||
| dc7f5e3dbc | |||
| c3f45471c7 | |||
| c47d5565c2 | |||
| fd4f3147f9 | |||
| 22e23e1e65 | |||
| a470b2cd4e | |||
| 8e8b635769 | |||
| 83b056738f | |||
| af13c580b3 | |||
| e35197c968 | |||
| 58992030dc | |||
| 677dfde22e | |||
| e1433cbf2a | |||
| 93e48fabdf | |||
| fb424d28b9 | |||
| 8b9017224f | |||
| 3b50a732b5 | |||
| 5852fe54a7 | |||
| 2fe84293b3 | |||
| 66d1de0821 | |||
| 148c0d9213 | |||
| 871a6f23e2 | |||
| b043e6bf52 | |||
| 4bbae73be1 | |||
| daddbadc06 | |||
| 4029b94ff2 | |||
| b74d1cb54a | |||
| 6a689e2ae4 | |||
| cf5059b006 | |||
| 337c5412b7 | |||
| 01b1f7529e | |||
| 820aeee659 | |||
| 4db5855b74 | |||
| 579f5d7c8a | |||
| 313256adff | |||
| f2a2e86db7 | |||
| 75206dd8ac | |||
| 8435063c3f | |||
| 8700e1ef65 | |||
| bba48f455e | |||
| 6ac0cd421a | |||
| 1a471a7c20 | |||
| e851abb5a5 | |||
| 85eb88a4a6 | |||
| fd61a0bfbf | |||
| 2513ade3c4 | |||
| 5b2ad5f87f | |||
| b4b731eaa4 | |||
| 2b4a354a8c | |||
| a1d9c5d86c | |||
| 744cc51140 | |||
| c73e24c3a2 | |||
| e3bec5039b | |||
| 85d9c91e5e | |||
| ca236da385 | |||
| c904a92c22 | |||
| 0952b6d68f | |||
| d09f932834 | |||
| ac4c5c6e28 | |||
| 0e3bcbe10d | |||
| 3b64176c0d | |||
| 782ecbd750 | |||
| 6b2e98b9be | |||
| 4cf1739604 | |||
| 369474a0bc | |||
| 24975d670e | |||
| 61ac1c4ae3 | |||
| 8b881ab0b4 | |||
| b291b04d62 | |||
| 5a5ce4e665 | |||
| 2317c17b4b | |||
| fb02bdb445 | |||
| 53d85ac7a2 | |||
| f25534ff0f | |||
| 463369f283 | |||
| 9377701ad1 | |||
| a98fce2160 | |||
| ccda1b4523 | |||
| ac6f80c274 | |||
| 5fd86b781b | |||
| 0d28292cfb | |||
| 8bf9d38711 | |||
| 4caf052199 | |||
| 82ea2cd86d | |||
| 89c0d5763c | |||
| 813409a8fb | |||
| 03b7445cb9 | |||
| 9f35801f64 | |||
| 526f5efb78 | |||
| b3071603d0 | |||
| 6f358dd8ea | |||
| a6a715b8c2 | |||
| 1ebbfe5d92 | |||
| 9e27921867 | |||
| f1057bb4a3 | |||
| 48434453e3 | |||
| 6e8597e3f7 | |||
| bb98fe824e | |||
| 70b57b078d | |||
| 8410e541b6 | |||
| 4bad752f30 | |||
| 68d677f028 | |||
| bee3150193 | |||
| 4025ef2070 | |||
| e64cc325d7 | |||
| 5c9d323fe9 | |||
| 5d55e558fe | |||
| 687f126f4f | |||
| 2bad06a234 | |||
| 61fbb76eeb | |||
| 38f8048309 | |||
| 540a0761ef | |||
| 051563522a | |||
| d86045c64e | |||
| 1a2e2f4d51 | |||
| f2fbe6dfee | |||
| 5dcecb55c2 | |||
| 3bd41013c0 | |||
| aa35099a4d | |||
| 6a45d83082 | |||
| 25397b94b3 | |||
| d1df48ebb1 | |||
| 4a7aa032c2 | |||
| 846986987d | |||
| b2d380afcc | |||
| 6135e83653 | |||
| f090bbc1c7 | |||
| a10d0e45c9 | |||
| 6d5671dd0e | |||
| 8a251fe39e | |||
| 24d93ea87d | |||
| 93aa4f086e | |||
| a357f5a1b8 | |||
| 39c0af46b0 | |||
| 4e4048be4e | |||
| b5e2c628cd | |||
| 6b8b69d324 | |||
| 36993703f6 | |||
| 713f1239c6 | |||
| cb3dffc7fc | |||
| c0065765d4 | |||
| 37621f8548 | |||
| 8e18a5fb4c | |||
| a0d96d5a74 | |||
| 0c8620e944 | |||
| 7fbc883ec0 | |||
| 6561a40f2c | |||
| 0427d08ede | |||
| a7aec70bc1 | |||
| ad6918d71e | |||
| c7d36ac06e | |||
| 9428cf0d06 | |||
| ec17e58eed | |||
| 2723f2150b | |||
| 9ced45c714 | |||
| 42e7233aaa | |||
| 4e54e4c22a | |||
| 565859b2aa | |||
| fe9ef2e85d | |||
| 0a5fedf089 | |||
| 45a9235738 | |||
| fd43c75811 | |||
| 4271f42685 | |||
| 016bfcddad | |||
| 26d18031f2 | |||
| 4307ff5bf0 | |||
| 648d6b2662 | |||
| 6f4a3701e7 | |||
| 2486aee24d | |||
| 19c9d7d59d | |||
| abd640d36b | |||
| a085e9ed54 | |||
| f790ef5a2b | |||
| d6ec588c11 | |||
| ed031c6dc1 | |||
| 0a871d57cc | |||
| f1653d4643 | |||
| 58bc040953 | |||
| c040206d3a | |||
| e0af49f638 | |||
| 7d1f218523 | |||
| c779701d26 | |||
| dc7b425dc5 | |||
| 9be00cd546 | |||
| 2caf8d6a4e | |||
| 27c4069187 | |||
| 4b2c0b0771 | |||
| 7e1216a3d3 | |||
| a02e3d2ebd | |||
| 84297ff473 | |||
| 1ffad1ebaf | |||
| 2db99e7807 | |||
| d7fdfd6d71 | |||
| 241053e1a8 | |||
| 6da8396c76 | |||
| 79e0a1b94c | |||
| 4d94700375 | |||
| 1672217dd9 | |||
| 313c90ff85 | |||
| 227da8dce4 | |||
| 84111996b4 | |||
| b23699f0c1 | |||
| 0c0610c6c5 | |||
| fb39aa32bb | |||
| ca25c6075b | |||
| a9f474b24d | |||
| 1354da01e6 | |||
| 6151a1ca7f | |||
| bbe1350f7d | |||
| d9c104613c | |||
| 4770b32287 | |||
| 89cfe1ef57 | |||
| 72d29030d9 | |||
| 5cf98922fb | |||
| 6a2f2b4efe | |||
| 69cb8c5a0a | |||
| 1ba9513a4e | |||
| 1095e47e6f | |||
| 0ebda97b03 | |||
| 2d5ef36a7f | |||
| 39cbfb84ae | |||
| 73a56830b0 | |||
| d924f73ceb | |||
| c44926c2cd | |||
| 11a852d49c | |||
| 2b30f5591c | |||
| d3661fa9d3 | |||
| a1c66e3808 | |||
| f3fc393617 | |||
| 3f6909866f | |||
| 3adcfd6b4a | |||
| d42be69735 | |||
| 9ed3328496 | |||
| 3e5bd096cf | |||
| d5e4d8637c | |||
| c5883ebe8f | |||
| e6d8115e52 | |||
| a9d70fe27e | |||
| 3fb314a1cb | |||
| e898639168 | |||
| ef9a17a28a | |||
| 16141a7104 | |||
| e496f4f3e2 | |||
| 5d42439bf4 | |||
| b60681e9bd | |||
| 25684bf4f6 | |||
| 8d57ef2be4 | |||
| 6143f792f3 | |||
| f0c5b603ee | |||
| b61b45177a | |||
| afd19a7307 | |||
| 155a348802 | |||
| 733de44d03 | |||
| 228532b03e | |||
| 54bd10a1db | |||
| 674f158877 | |||
| a07d7ffdec | |||
| aadc85cda0 | |||
| 43f6d79554 | |||
| fd1aea420e | |||
| dbdad6fce8 | |||
| f48231e309 | |||
| d24189e8aa | |||
| 5baa08eb0c | |||
| 95eb310a7f | |||
| d112d6308c | |||
| 00da650524 | |||
| bb3aae46c5 | |||
| 1ca8eeeb50 | |||
| 5d4eef50e4 | |||
| 2b2123f14d | |||
| 8b91b815ba | |||
| 5824c8ffd0 | |||
| 8788867572 | |||
| f2193ff45c | |||
| 1091142614 | |||
| ef22387440 | |||
| 61ff04119e | |||
| bb7252042e | |||
| c95e267cdd | |||
| 059547e37c | |||
| 6dc0e4f5c3 | |||
| 8fc9685a1b | |||
| 7744568eb8 | |||
| 30b6d1cd1d | |||
| fe9585500d | |||
| 1ddba4a76f | |||
| 53b83909ed | |||
| af75060001 | |||
| 315210fe29 | |||
| 284c646638 | |||
| 39df3d7bad | |||
| 8f75f7332d | |||
| c3edf44cb4 | |||
| f933716bf5 | |||
| b37c0fbb95 | |||
| 3d057781de | |||
| 043180a0fa | |||
| f345212468 | |||
| a6c367ba24 | |||
| 1a7eb9f6d8 | |||
| a22fffc8b5 | |||
| 7e036cc65f | |||
| 7a083e7ce4 | |||
| c6aef98361 | |||
| 639b7d9374 | |||
| 23f7c1522e | |||
| 2e4bc5e218 | |||
| a556237963 | |||
| e8488eb406 | |||
| 5363842c4d | |||
| 4c5d783699 | |||
| 64d067d5a1 | |||
| 1845a65085 | |||
| 86bad5cb3e | |||
| ad4c88b535 | |||
| 55dde26aae | |||
| a93c85ebc9 | |||
| cd45046724 | |||
| 804be6d5e4 | |||
| 344782099f | |||
| 4ffff84540 | |||
| dffa3b7986 | |||
| 0f7bc9be52 | |||
| 05fc8ed5db | |||
| 74a8779c49 | |||
| 40631a753f | |||
| 5cf266c5be | |||
| 0f3eb42332 | |||
| 1fa8395847 | |||
| 1b6e283ac1 | |||
| 7b9504c5b4 | |||
| 2b52e21ccf | |||
| c49050ea69 | |||
| 53037c96cf | |||
| 000e5fa105 | |||
| 3ccad9ada9 | |||
| 73bd3e513c | |||
| 2c2a24c31b | |||
| 28a1c254d9 | |||
| 203ad6b565 | |||
| 0969bb9824 | |||
| c6ae7729d1 | |||
| af719dd8c2 | |||
| f87e257233 | |||
| 83d7535d84 | |||
| 811cc9c028 | |||
| f14fbfe087 | |||
| 446099b1f9 | |||
| bad927e283 | |||
| ccbb3dca9f | |||
| d47b947acf | |||
| 6332db8e86 | |||
| d829d43f2f | |||
| 8944698df1 | |||
| ccf5f2f60e | |||
| d381f579d3 | |||
| 995c0a7afc | |||
| ebba516603 | |||
| 9289d80e32 | |||
| d366e10cb9 | |||
| 7c30c2f945 | |||
| 4ae9821185 | |||
| 2283c90da1 | |||
| a32d2c6f4d | |||
| 208edfcebd | |||
| 6ec869505e | |||
| 4135040007 | |||
| 67c3883cb4 | |||
| 30b3be8194 | |||
| 594a2e759a | |||
| 46e6a048d4 | |||
| 426fe6e3c1 | |||
| 4bfaf1728d | |||
| 83aaa9e8f0 | |||
| 6adf26f4d9 | |||
| ac9ffa3f0f | |||
| 7a94216b3a | |||
| fd361ef5cb | |||
| 314e20ea18 | |||
| 48bf21a537 | |||
| f6de9918b5 | |||
| c37ecab029 | |||
| a50d7e2c60 | |||
| 38ff515d68 | |||
| ff2cb31f0f | |||
| 85fc467845 | |||
| 1289a6e14f | |||
| a88d523559 | |||
| 48e2a79d8f | |||
| 56c671149e | |||
| d37621044c | |||
| f3087773e3 | |||
| 94681fd47d | |||
| c2678efc06 | |||
| e682a77858 | |||
| 89e3ce06fa | |||
| d1afc9c10a | |||
| f3ac263238 | |||
| 2d3c1b7702 | |||
| e028eadf60 | |||
| 1cd94b4f7a | |||
| fd70f456e9 | |||
| f03b23497a | |||
| 1842afa7c6 | |||
| 41901be95f | |||
| 93261c63b5 | |||
| 95dfe5361d | |||
| e5b27af055 | |||
| b2c48af53f | |||
| b0f0a34672 | |||
| 9c477710b9 | |||
| 3c01673741 | |||
| 28743efb60 | |||
| eb201d3474 | |||
| b54d34127e | |||
| fcfc9572f1 | |||
| c0f0084e56 | |||
| 7e3162d287 | |||
| c231184c28 | |||
| 08294f5f39 | |||
| 048afdc232 | |||
| 8779de448d | |||
| 3770f07720 | |||
| f53180a960 | |||
| 6b6c1b98be | |||
| 65405da8d1 | |||
| 3df54eac21 | |||
| 68c1cd83a7 | |||
| be3696ff8c | |||
| 9666deca6d | |||
| 87d25336e7 | |||
| 11f4b3d869 | |||
| 2ed7f152b7 | |||
| 6e21f19bd5 | |||
| 8e205207b0 | |||
| 892ff38a3f | |||
| 3428b95672 | |||
| 64cb966683 | |||
| 8bcff36745 | |||
| d985a66eff | |||
| 16f855f173 | |||
| 31500076d1 | |||
| d955f0e3d8 | |||
| 4615418000 | |||
| 9ad0799b48 | |||
| 60b1dcf5ee | |||
| d0c9e17090 | |||
| e829c2031c | |||
| 85ea71d9aa | |||
| e14dd36a0a | |||
| cf210c5a5a | |||
| ce571bad81 | |||
| 55b0fb69ac | |||
| e04f3e317e | |||
| 8f2b6cf44e | |||
| ee0ac7cbed | |||
| 87fd1a5f38 | |||
| c0ef2eb559 | |||
| b7cc592fb1 | |||
| 1d2c3a748b | |||
| 405529e8f6 | |||
| e17959ed65 | |||
| b599f95564 | |||
| 16c8721d6c | |||
| 6c115804e8 | |||
| 43c69d4288 | |||
| b8dcfcf900 | |||
| f77aeded6f | |||
| faf6ea83fa | |||
| cb7e54acaf | |||
| 87f9837939 | |||
| 380447dd41 | |||
| a8667836b3 | |||
| 31d0d6c7c9 | |||
| 77cd519be2 | |||
| 361902d59d | |||
| 5accc2a923 | |||
| 53eb1649d5 | |||
| 4cbda25000 | |||
| 5a10a8dd46 | |||
| ea4c140ad0 | |||
| a67b4a5059 | |||
| 95bdbc590c | |||
| d249c77b18 | |||
| e07914e9b3 | |||
| d831624d43 | |||
| 827791574d | |||
| b458a254a9 | |||
| 1bbdabc42f | |||
| 91fd93c724 | |||
| 186a336232 | |||
| 27c697c1d2 | |||
| 974fd19b40 | |||
| d384442fb3 | |||
| 46dbd016f4 | |||
| 0cf0c7a27c | |||
| 5489237f11 | |||
| aa16370fc5 | |||
| b75554ddcb | |||
| a31281df40 | |||
| 51d9e7fcb1 | |||
| e2b15c9b4f | |||
| 6bbb968128 | |||
| 91ba2a9282 | |||
| 21d7dd873e | |||
| 3f0f189a3a | |||
| dc49dd0a94 | |||
| a7c6e36ec3 | |||
| c8976daf96 | |||
| 7aca2a1b36 | |||
| 63f9e26b2e | |||
| 547783e0e0 | |||
| 86109127b8 | |||
| 6df195aa94 | |||
| cbdea7965d | |||
| 0a74546a98 | |||
| fde6e5f1fb | |||
| 06ea69ae4a | |||
| 4728885437 | |||
| a7e26055b2 | |||
| 45e6ef09a8 | |||
| d9f07c4de4 | |||
| 98cc0b9dfc | |||
| f84076d80c | |||
| 5f85810345 | |||
| 52ad526b87 | |||
| 8446773037 | |||
| be10718dfc | |||
| 8939f0dad7 | |||
| 3c1419a22f | |||
| 211c52ccc1 | |||
| 99b73f91b8 | |||
| 9da96b9e92 | |||
| 7f87af5a08 | |||
| e2659c87f4 | |||
| a8cb0a0e9c | |||
| 34583c834d | |||
| 1d61d4dad4 | |||
| 434bfdde67 | |||
| d5a8731850 | |||
| d3738f2f31 | |||
| 570a8ee97d | |||
| 9168af4850 | |||
| d8a9ef8187 | |||
| 475239bd02 | |||
| ea3042e1d7 | |||
| 39d631b056 | |||
| 6e06e05f12 | |||
| 4d8f8f19d4 | |||
| 384e341452 | |||
| aa46285b8f | |||
| fae00992c8 | |||
| 47477808e4 | |||
| be16f93ba0 | |||
| a153f0d87d | |||
| 179dd6339f | |||
| 4c662a30ef | |||
| 28372af5a9 | |||
| 1f93cce2c5 | |||
| 62cc534153 | |||
| 4df1ec867c | |||
| ff195ad272 | |||
| 00ca21243b | |||
| 4d9c73ab1f | |||
| ddf98937d6 | |||
| 5acd7c3e99 | |||
| 0f35017955 | |||
| 2235524f97 | |||
| 17a8b38484 | |||
| 7651b5a11d | |||
| 775a066a9a | |||
| 5cdbfc0af7 | |||
| 4a8e9fef99 | |||
| 05f68d0b1a | |||
| d82a685d7f | |||
| 105bcf23be | |||
| a26ceeb76d | |||
| 9f94545283 | |||
| daac71ac9a | |||
| 2db34c4d7e | |||
| 5212f6d0b7 | |||
| 3ec502d862 | |||
| 1d5bca7962 | |||
| 4b9b4c1427 | |||
| dfc4667910 | |||
| 5be2eb1c9a | |||
| 5e1e6657d1 | |||
| 2a2019b363 | |||
| a8de386af6 | |||
| 63354b00eb | |||
| 822460e5ee | |||
| dbf6eeb144 | |||
| 47295ec417 | |||
| 0e60b46db0 | |||
| 8d7175875a | |||
| 024d057e03 | |||
| ec20fb453b | |||
| 06768833cc | |||
| 51d1efead6 | |||
| 04ff495066 | |||
| df408505e0 | |||
| 7674e23580 | |||
| 689cf3171d | |||
| ac23dda8db | |||
| da7934555c | |||
| 0b9b0e7f13 | |||
| a430b66a8d | |||
| 11e4542746 | |||
| 7238b89437 | |||
| e2dcc3c0ea | |||
| 0e944f7d8a | |||
| b391f4bc12 | |||
| 65f066d391 | |||
| cbe81a8f0a | |||
| 9957739dc4 | |||
| d0eb84ec51 | |||
| df0e211d01 | |||
| a3eef29b26 | |||
| 3614812680 | |||
| 8cdae41051 | |||
| b59833e8f0 | |||
| a4586b8bf1 | |||
| 3fbdfbe7a4 | |||
| 73a5fb1648 | |||
| 71fc7c4ab6 | |||
| d57b39da6e | |||
| 4bf13f3c70 | |||
| 4297665c93 | |||
| 7131ae2d91 | |||
| 103dc9704a | |||
| 7efa79637c | |||
| f33666b848 | |||
| 2acd6c8b40 | |||
| 7329c9d0bb | |||
| 4a7cc06d05 | |||
| b2bb29911c | |||
| 028a723a31 | |||
| 05705be7c3 | |||
| f89c0bb07c | |||
| ca402cb604 | |||
| 47b9f15b0b | |||
| 2104df5a83 | |||
| 5527926508 | |||
| f82f3fa858 | |||
| 69d4514716 | |||
| 0a49fcb22a | |||
| 89d50ed5f1 | |||
| 9224d1e017 | |||
| 9b2c954f8e | |||
| d718c88353 | |||
| b2e1d4cc61 | |||
| e3b81d0b36 | |||
| cbfe19aebf | |||
| 15269713cc | |||
| cbba373d7d | |||
| 2a6544b794 | |||
| 2eedfad833 | |||
| 55b946e784 | |||
| 1839e1ac42 | |||
| 67bc03bcc1 | |||
| f0f04ce3ff | |||
| 16caeb5400 | |||
| 9a6995343b | |||
| ec07334d14 | |||
| c859321cfd | |||
| d68db98d62 | |||
| b89f55f9b9 | |||
| 58f42f1441 | |||
| 3df0e6fda1 | |||
| 79093bf61c | |||
| 78e2c223f2 | |||
| deea1249c4 | |||
| a513fbf592 | |||
| d6eb675b89 | |||
| 10df195630 | |||
| 6dfcec6e8b | |||
| 8ffd091e53 | |||
| 8c7f1de80c | |||
| 830db36928 | |||
| ae68cc95bb | |||
| a123a8301f | |||
| 0ce1c120f9 | |||
| 67728851a3 | |||
| 785db03650 | |||
| 2fe02a2cc3 | |||
| 7a747774fd | |||
| 8a88a8a9ef | |||
| 42456bac0f | |||
| 0cffd76296 | |||
| d8373bc488 | |||
| 5953fd71d3 | |||
| de6837adb0 | |||
| 23dd575ce1 | |||
| 1f8cdbaf62 | |||
| e312c3147a | |||
| 128e7fccdb | |||
| 3a323551eb | |||
| b9d9797734 | |||
| 26d94c0519 | |||
| 6befc2b7a2 | |||
| 9db671b83a | |||
| c4205c1c11 | |||
| 93d47bb0c2 | |||
| 218ed1ce13 | |||
| bb12da6777 | |||
| c4452c99ae | |||
| 5ef0ad9a3e | |||
| 113a3eb5cf | |||
| eec386cbf4 | |||
| b3e8b41e0e | |||
| 1db275707b | |||
| 64b730a22e | |||
| 8522775569 | |||
| e81b77cbcd | |||
| 8498b79493 | |||
| 7c1160de92 | |||
| c517b6db3b | |||
| 30f3a6f450 | |||
| 2ed9d68c08 | |||
| d45c2eb5b6 | |||
| fc1c1ea690 | |||
| 18e17f2932 | |||
| e70ea6f202 | |||
| c4026c8e78 | |||
| a58b2efaf9 | |||
| e917ae4198 | |||
| 3832ac3965 | |||
| bba9de7b76 | |||
| d2de2c7093 | |||
| 53aed4c7f8 | |||
| e8bd839281 | |||
| 7d07e34d6b | |||
| f6f97e69eb | |||
| 0fe5d9d628 | |||
| e665857aa8 | |||
| 00f6b878b3 | |||
| 7cddf8ae6a | |||
| 3383a84fa6 | |||
| 8e120bca77 | |||
| 66d235a873 | |||
| dca9266697 | |||
| b3131169ad | |||
| e83bcf0fd9 | |||
| 9137244fcf | |||
| 3a32aab066 | |||
| 5fd4e2f008 | |||
| 0bc460db56 | |||
| 5a34244854 | |||
| 9676cabcb8 | |||
| 44b02961ac | |||
| 66f0b49ca7 | |||
| 24ed0fd479 | |||
| 8f6656d657 | |||
| 19e08942ed | |||
| 0865b9bbc5 | |||
| 00a5dbf42c | |||
| acae9ee602 | |||
| 93e0787911 | |||
| 5c354db1b2 | |||
| a5ca8781ff | |||
| ba1636033e | |||
| d8c063f59b | |||
| 30b99abfb1 | |||
| 8a51582d8a | |||
| 72635c8711 | |||
| 7bec653b55 | |||
| 9d30b6e29a | |||
| 77d4a9f711 | |||
| 8eec3cddb5 | |||
| d633029d08 | |||
| 15a27e234c | |||
| 174846f110 | |||
| 2aed64d1c1 | |||
| e36f24dc63 | |||
| 74f4dacbe4 | |||
| 4d7a5f03ab | |||
| 40dcbd1229 | |||
| 30686477f1 | |||
| 7beddf3019 | |||
| 8e8737c4df | |||
| 454afd5d1b | |||
| 5d88e86462 | |||
| 02666b7da4 | |||
| 5342dae5b3 | |||
| 2b0fd17fbf | |||
| f0848e23c7 | |||
| 3dc6e274f0 | |||
| 2c6d9e699f | |||
| 27ecde2f17 | |||
| 05a685fb24 | |||
| 6b272e8a6f | |||
| 7d9f57299c | |||
| 263f68554e | |||
| c7a924e83b | |||
| 74a71f4d8e | |||
| 9b9fb325bd | |||
| b7b738c92b | |||
| b8a539a67a | |||
| 23f7a540fb | |||
| 3d487be59e | |||
| 6ef6caaca4 | |||
| 87a222748f | |||
| ef0a9a4a1e | |||
| 6c674e86e5 | |||
| 55e6e80433 | |||
| 86bbad5b3d | |||
| e02d2c8af2 | |||
| 6907c6dfe4 | |||
| 1a51a355f2 | |||
| 92227a3b7f | |||
| ab0d5c8967 | |||
| e13db20c85 | |||
| e4b4a7b0c5 | |||
| 1fa23bdf16 | |||
| f8905a0490 | |||
| 5fe6757f8a | |||
| f3ac71ed3e | |||
| 8aef6ebfbc | |||
| 31d593a529 | |||
| d5b47ecdce | |||
| f5020f8dc0 | |||
| def774c9ef | |||
| 2dea8309a7 | |||
| d153a734cb | |||
| a220667f1b | |||
| 62b648c70f | |||
| 9f8bde7078 | |||
| c593b4180a | |||
| 3e2177b402 | |||
| d9bf09bb9b | |||
| 21c1d3c191 | |||
| 7da590ca76 | |||
| 97a298375a | |||
| 7d35c9a8eb | |||
| d77aaded39 | |||
| fae1b4dec1 | |||
| e4c47de90a | |||
| 6a5de6606c | |||
| 8132920ed8 | |||
| 26e8eb8c11 | |||
| 3647f3130f | |||
| f52a330b16 | |||
| a669144d16 | |||
| 78e835dd5f | |||
| da8a7041d1 | |||
| 3aa103e114 | |||
| 6aca198428 | |||
| 085ed59334 | |||
| 3f3bb4d3b7 | |||
| 148e3dc65b | |||
| 063b102461 | |||
| 558620cdfa | |||
| 7b62c9e8a4 | |||
| b44d65d265 | |||
| 1197693591 | |||
| 81f91f3324 | |||
| f78f6634fa | |||
| 28eee609de | |||
| a3e1843e8b | |||
| c9b8607c9f | |||
| fe94d75b1b | |||
| 5aadf88ae3 | |||
| 4e038142da | |||
| d75e95a23d | |||
| a019227ddc | |||
| d3b10a6711 | |||
| faa14fa91d | |||
| 85dced9cd1 | |||
| e45df6fa6d | |||
| 8bbf9b50b4 | |||
| 12794a499d | |||
| 3d31f73453 | |||
| 99f30fe09a | |||
| 11c64f3035 | |||
| aa65af10b6 | |||
| 1afc8d20a0 | |||
| 42b4d7ce5f | |||
| c1c53af855 | |||
| 740c88c506 | |||
| 270d27be73 | |||
| dc98c2c1fe | |||
| e6079ee275 | |||
| 26b88a55e0 | |||
| b5721fe6f3 | |||
| ec99cae3d9 | |||
| cceae6770c | |||
| 4cfa658fde | |||
| 8e58349bfa | |||
| 68a08b1f62 | |||
| 20a30b303b | |||
| 373f294fe3 | |||
| 5459a540e7 | |||
| 221181092e | |||
| 326e01d548 | |||
| c7572f0624 | |||
| b94ce542c3 | |||
| 6a303ae50a | |||
| 59721a3f1a | |||
| bd5ecf358a | |||
| 2617fcdcfd | |||
| dd5e4adc73 | |||
| c46c41db5a | |||
| aafd3c8d35 | |||
| cb6d531300 | |||
| e7b11a7ceb | |||
| 54c6c2e6cd | |||
| c7ef32bf3c | |||
| 824816cb94 | |||
| a138425298 | |||
| 9888aa8c08 | |||
| da0dcf65b3 | |||
| c9d875e3fa | |||
| bd44bb4534 | |||
| fda0a918f0 | |||
| 31599c5da9 | |||
| 7e1fbb3017 | |||
| 08b2f6f998 | |||
| daeaa800d8 | |||
| 9c4baf9cad | |||
| a1f01d2def | |||
| dde6e0859b | |||
| 7322006455 | |||
| c193a86a4c | |||
| 58fcca58fc | |||
| ffc477091f | |||
| 46620e2da5 | |||
| c43ba46c97 | |||
| a2fdefb6a6 | |||
| 0aea814a23 | |||
| 5c605be45c | |||
| 1e34f3ef38 | |||
| 569cd4dd42 | |||
| 203ab05d12 | |||
| 471c68474a | |||
| 76cc9614df | |||
| e53a35eebf | |||
| 96f60caa77 | |||
| e608257c1a | |||
| 05f31f40eb | |||
| 0b73633a66 | |||
| 2f5c4413e1 | |||
| c6e93b9870 | |||
| f64297c256 | |||
| edb9bac503 | |||
| fa64191082 | |||
| 4a5c0e7ff0 | |||
| aa54e47029 | |||
| 7bcb6dd7cb | |||
| 9651db67ce | |||
| 3ebb41c5ad | |||
| 23d91c7b37 | |||
| b644418b11 | |||
| 86c4045073 | |||
| 42f493b2c3 | |||
| 5a766c583c | |||
| 776e65bc5e | |||
| 55939f6320 | |||
| a1ea6b21d8 | |||
| c61357ec6d | |||
| d84b4e4f7d | |||
| 19bae2af96 | |||
| daff760280 | |||
| e161c26c35 | |||
| b99f634939 | |||
| a93b3dab1b | |||
| 5a16a50b3f | |||
| 21888829cc | |||
| dffef6f839 | |||
| 4ef7195d38 | |||
| 627d6f8a28 | |||
| 178ebf3ed0 | |||
| 979e91a2eb | |||
| 4dec688e0b | |||
| 1247dbe999 | |||
| f6f6318e02 | |||
| 6c7341b9f3 | |||
| 71e9caef9c | |||
| dfba9d3650 | |||
| 45cfe1a817 | |||
| 68066cdd48 | |||
| d1da77d6bc | |||
| 6cd97d2cb9 | |||
| dcae882009 | |||
| ee45bc713e | |||
| 5936e00acc | |||
| d12e8cbe97 | |||
| 60140087a5 | |||
| 74f4849144 | |||
| b8c36b034b | |||
| d51183be51 | |||
| fd3bb37c48 | |||
| f990095ddf | |||
| c86b99e69c | |||
| 70b15d128a | |||
| 42ce3cb405 | |||
| 98fb9c89de | |||
| 2bc2b632c6 | |||
| 2b4c6514c3 | |||
| f3e2f10478 | |||
| b43fa38350 | |||
| 0d4165421c | |||
| b02e8a9502 | |||
| b625e40eb0 | |||
| 47729e3e43 | |||
| ac0426d97d | |||
| 0619274d00 | |||
| 24b9e713f2 | |||
| b3736617c4 | |||
| 63ee99fde9 | |||
| 837d5803c8 | |||
| 2d1f24ca0f | |||
| 84a1f40115 | |||
| abb40b3ad7 | |||
| dfd54b7b54 | |||
| c82cde5e16 | |||
| 5ea4c90292 | |||
| fa3bdb5464 | |||
| 12abf494d8 | |||
| f5e8a60a8f | |||
| c53adedbe0 | |||
| 3b6831ea93 | |||
| 2f19af6a3e | |||
| 0f5a4c10ef | |||
| dfcaacee80 | |||
| 2ac3c1fe6e | |||
| d8c70c2498 | |||
| 16c15548c1 | |||
| d0fbf68d90 | |||
| 230599417e | |||
| 4c235f0427 | |||
| 4045a16e27 | |||
| 7f91e85df8 | |||
| dfb3285e18 | |||
| 0280059c13 | |||
| 5fdc3eca7e | |||
| 95cee1676a | |||
| 70580cc6ae | |||
| f56eb0d791 | |||
| 4534a729c7 | |||
| 62a6c243fe | |||
| 5feb5ee015 | |||
| 7907d05847 | |||
| 1dfa95d457 | |||
| b6b52ffdb7 | |||
| 97cb73cb73 | |||
| 3f3226391a | |||
| 91c873c25f | |||
| b38e28e50e | |||
| 65fc3eb362 | |||
| 3955fbdc64 | |||
| 441b72158d | |||
| 8e1457d605 | |||
| da4215afbd | |||
| 6e2007aeca | |||
| 4ba01c6cf1 | |||
| 1dd5f63a11 | |||
| 467921dbf6 | |||
| 99b2140838 | |||
| 63b5e28537 | |||
| 109eefa7e3 | |||
| 380791211f | |||
| bffd7c8d24 | |||
| aaab06f21d | |||
| 4c87b62b5e | |||
| e34e479c33 | |||
| 665f7c849b | |||
| 88b9eed559 | |||
| cb162a8f1e | |||
| db5d3aa9bd | |||
| 9db1ad32f9 | |||
| 630286665b | |||
| 8b08c1d113 | |||
| 6fcfa6cac0 | |||
| b1da8bbc4c | |||
| 7d56e71f77 | |||
| 68e02817a0 | |||
| 58bb2d5f93 | |||
| ee263914aa | |||
| a887fc8153 | |||
| 12d9e6b618 | |||
| f1b9ce61e1 | |||
| 3f2c7542f8 | |||
| 132c1a9a8b | |||
| 801c0ac47c | |||
| 24550236be | |||
| 3f46d6d16e | |||
| 5d6cf5789a | |||
| fca66222df | |||
| 2e3120cd46 | |||
| fa2b03b7fa | |||
| b141b3c95e | |||
| 83a2f07ba9 | |||
| ec2dd67d89 | |||
| 6da071c88d | |||
| 98ba214dfb | |||
| 5b86c96adf | |||
| a43f9d78c4 | |||
| bc8b32ebab | |||
| 363efd3836 | |||
| c1817505f8 | |||
| 27fac748cd | |||
| 56739945fa | |||
| 8a5e7accaf | |||
| 8c8ae57103 | |||
| c3ab43a9fc | |||
| a2d1d83457 | |||
| ce01adc5b7 | |||
| f0598c9fec | |||
| 19c0d325ca | |||
| 97d0cc2fb4 | |||
| df3a80e17e | |||
| 3eacdff1d6 | |||
| cd63d2622c | |||
| 2bf91ea453 | |||
| 7e8cbc542b | |||
| 55e0d734df | |||
| a42c7e4735 | |||
| dd264617d7 | |||
| 2167b1fc6b | |||
| 9b3f1c3f9c | |||
| e51014a5bc | |||
| 58e5f96eda | |||
| 74679b4b41 | |||
| 2a0f727cba | |||
| 9937c01cde | |||
| ebf3e4fea7 | |||
| 8e3839203b | |||
| a53bc720cb | |||
| ab62f61256 | |||
| a2b737e6a0 | |||
| 893e07ea16 | |||
| 78cfb9b7f0 | |||
| fae42bbca3 | |||
| d95b13664e | |||
| f7fe38fa25 | |||
| 38ed49d814 | |||
| fb08369aec | |||
| a071cef46a | |||
| 7438576bb1 | |||
| 571d510423 | |||
| 8dd4b58227 | |||
| d9402bc24d | |||
| 0513ed16bb |
@@ -2922,3 +2922,61 @@
|
||||
[8.3.1]
|
||||
* Fix crash in postgresql pgvector extension
|
||||
|
||||
[8.3.2]
|
||||
* Bring back immich vectors hook in postgres addon
|
||||
|
||||
[9.0.0]
|
||||
* UI redesign
|
||||
* notifications: email notification when server reboot is required
|
||||
* notifications: email notification when cloudron update failed
|
||||
* notifications: email notification for low disk space (90%)
|
||||
* node: update to 22.20.0
|
||||
* docker: update to 28.1.1
|
||||
* s3: automatically abort old multipart uploads
|
||||
* notifications: validate domains configs
|
||||
* ldap: automatically detect pagination support
|
||||
* ubuntu: alert for 20.04 support being deprecated
|
||||
* domains: vanity nameservers
|
||||
* token: access can by restricted by ip range(s)
|
||||
* sendmail: requiresValidCertificate option for using mail server domain
|
||||
* mail: update haraka to 3.1.1
|
||||
* sshfs: implement rm via ssh
|
||||
* multiple docker registries
|
||||
* mail: rename delivered -> sent and received -> saved in event log
|
||||
* graphs: replace collectd with custom collector
|
||||
* graphs: live graphs
|
||||
* graphs: add system disk and network graph
|
||||
* profile: drop gravatar support
|
||||
* login: suppress notification of impersonated users
|
||||
* mongodb: reduce verbosity of logs
|
||||
* redis: disable by default when optional
|
||||
* apps: fix issue where operations on stopped apps errored
|
||||
* eventlog: Fix incorrect eventlog that the update crashed
|
||||
* database: change charset to utf8mb4. this allows emojis everywhere!
|
||||
* mail: add brevo as relay provider
|
||||
* mail: add rbl6 check
|
||||
* eventlog: mail server change log
|
||||
* profile: avatar cannot be changed when profile is locked
|
||||
* app backup: no more part alters app state. runs completely in background
|
||||
* system: disk usage is not collected in background. new disk ui, computes space on demand
|
||||
* backups: multiple backup targets
|
||||
* port bindings: add `enabledByDefault` property in manifest
|
||||
* backups: store integrity information and perform validation
|
||||
* reverse proxy: remove OCSP support. this is being deprecated in favor of CRLs
|
||||
* sqlite: fix issue where dump was also logged when backing up
|
||||
* backups: remove noop backend
|
||||
|
||||
[9.0.1]
|
||||
* redis: update to 8.2.2
|
||||
* Split the ubuntu version and cloudron version
|
||||
* Restructure sidebar menu items
|
||||
* eventlog: fix display of backup (sites) events
|
||||
* app archive: fix download config
|
||||
* graphs: fix performance issue when selecting apps
|
||||
* Support overwrite DNS in app install dialog
|
||||
* encryption: do not allow password and hint to be the same
|
||||
* Add better grouping to mailbox owner select
|
||||
* eventlog: display task log link when available
|
||||
* add ephemeral port warning
|
||||
* rsync: fix integrity computation
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
'use strict';
|
||||
|
||||
const constants = require('./src/constants.js'),
|
||||
fs = require('fs'),
|
||||
fs = require('node:fs'),
|
||||
ldapServer = require('./src/ldapserver.js'),
|
||||
net = require('net'),
|
||||
oidc = require('./src/oidc.js'),
|
||||
net = require('node:net'),
|
||||
oidcServer = require('./src/oidcserver.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -35,6 +35,7 @@ async function setupNetworking() {
|
||||
// this is also used as the 'uncaughtException' handler which can only have synchronous functions
|
||||
function exitSync(status) {
|
||||
const ts = new Date().toISOString();
|
||||
if (status.message) fs.write(logFd, `${ts} ${status.message}\n`, function () {});
|
||||
const msg = status.error.stack.replace(/\n/g, `\n${ts} `); // prefix each line with ts
|
||||
if (status.error) fs.write(logFd, `${ts} ${msg}\n`, function () {});
|
||||
fs.fsyncSync(logFd);
|
||||
@@ -55,7 +56,7 @@ async function startServers() {
|
||||
|
||||
async function main() {
|
||||
const [error] = await safe(startServers());
|
||||
if (error) return exitSync({ error: new Error(`Error starting server: ${JSON.stringify(error)}`), code: 1 });
|
||||
if (error) return exitSync({ error, code: 1, message: 'Error starting servers' });
|
||||
|
||||
// require this here so that logging handler is already setup
|
||||
const debug = require('debug')('box:box');
|
||||
@@ -73,8 +74,12 @@ async function main() {
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldapServer.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
await oidcServer.stop();
|
||||
|
||||
setTimeout(() => {
|
||||
debug('Shutdown complete');
|
||||
process.exit();
|
||||
}, 2000); // need to wait for the task processes to die
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async function () {
|
||||
@@ -84,11 +89,15 @@ async function main() {
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldapServer.stop();
|
||||
await oidc.stop();
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
await oidcServer.stop();
|
||||
|
||||
setTimeout(() => {
|
||||
debug('Shutdown complete');
|
||||
process.exit();
|
||||
}, 2000); // need to wait for the task processes to die
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
|
||||
process.on('uncaughtException', (error) => exitSync({ error, code: 1, message: 'From uncaughtException handler.' }));
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
dist/
|
||||
node_modules/
|
||||
|
||||
# will get generated on build
|
||||
public/theme.css
|
||||
public/theme.css.map
|
||||
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
dist/
|
||||
|
||||
# these are not done yet
|
||||
public/translation/ja.json
|
||||
public/translation/pl.json
|
||||
|
||||
@@ -1,153 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title>Cloudron Setup</title>
|
||||
<meta name="description" content="Cloudron Setup">
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- contains all thing already using import statement -->
|
||||
<script type="module" src="./src/modules.js"></script>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="./src/theme.scss">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-ui-notification.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/js/angular-translate.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-loader-static-files.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-storage-cookie.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-storage-local.min.js"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/js/showdown-1.9.1.min.js"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/activation.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/js/client.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/js/utils.js?%VITE_CACHE_ID%"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="setup" ng-app="Application" ng-controller="SetupController">
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> Cloudron is offline. Reconnecting...</a>
|
||||
|
||||
<div class="main-container" ng-show="initialized">
|
||||
<div class="row" ng-show="view === 'owner'">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px;">
|
||||
<form role="form" name="ownerForm" ng-submit="owner.submit()" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Welcome to Cloudron</h1>
|
||||
<h3>Set up Admin Account</h3>
|
||||
<p class="has-error text-center" ng-show="owner.error.generic">{{ owner.error.generic }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="form-group" ng-class="{ 'has-error': ownerForm.displayName.$dirty && ownerForm.displayName.$invalid }">
|
||||
<label class="control-label" for="inputDisplayName">Full Name</label>
|
||||
<input type="text" class="form-control" ng-model="owner.displayName" id="inputDisplayName" name="displayName" placeholder="Full Name" required autocomplete="off" ng-disabled="owner.busy" autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.email.$dirty && ownerForm.email.$invalid) || (!ownerForm.email.$dirty && owner.error.email) }">
|
||||
<label class="control-label" for="inputEmail">Email <sup><a ng-href="https://docs.cloudron.io/installation/#admin-account" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="email" class="form-control" ng-model="owner.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>A valid email is required for Let's Encrypt certificates. This email is local to your Cloudron. </small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (ownerForm.username.$dirty && ownerForm.username.$invalid) || (!ownerForm.username.$dirty && owner.error.username) }">
|
||||
<label class="control-label" for="inputUsername">Username</label>
|
||||
<input type="text" class="form-control" ng-model="owner.username" id="inputUsername" name="username" placeholder="Username" ng-maxlength="512" ng-minlength="1" required autocomplete="off" ng-disabled="owner.busy">
|
||||
<small>{{ owner.error.username }}</small>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': ownerForm.password.$dirty && ownerForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" ng-model="owner.password" id="inputPassword" name="password" placeholder="Password" ng-pattern="/^.{8,}$/" required autocomplete="off" ng-disabled="owner.busy" password-reveal>
|
||||
<small><span ng-show="ownerForm.password.$dirty && ownerForm.password.$invalid">Password must be at least 8 characters</span> </small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-success" ng-disabled="ownerForm.$invalid || owner.busy"><i class="fa fa-circle-notch fa-spin" ng-show="owner.busy"></i> Create Admin</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="view === 'finished'">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card" style="max-width: none; padding: 20px 40px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Cloudron is ready to use</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Before you start:
|
||||
<ul class="fa-ul">
|
||||
<li><i class="fa-li fa fa-users"></i>
|
||||
<b>User management</b>: Cloudron has a central user directory. When installing an app,
|
||||
you can set it up to authenticate against this directory.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-envelope-open"></i>
|
||||
<b>Email Configuration</b>: Apps are configured to send email based on the settings in the Email view.
|
||||
This saves you the trouble of having to configure mail settings inside each app.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-archive"></i>
|
||||
<b>Backups</b>: Store your backups on storage services completely independent from your server.
|
||||
You can use backups to seamlessly migrate your setup to another server.
|
||||
</li>
|
||||
<br/>
|
||||
<li><i class="fa-li fa fa-birthday-cake"></i>
|
||||
<b>Updates</b>: The Cloudron team tracks upstream releases and publishes app updates after testing.
|
||||
Your apps are kept fresh & secure.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a class="btn btn-success" ng-href="firstTimeLoginUrl">Proceed to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©2022 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Cloudron Admin Setup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" style="overflow: hidden; height: 100%;"></div>
|
||||
<script type="module" src="/src/activation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<script>
|
||||
|
||||
var tmp = window.location.hash.slice(1).split('&');
|
||||
const tmp = window.location.hash.slice(1).split('&');
|
||||
|
||||
// FIXME: implicit flow (response_type=code token) results in access_token query param. this is not secure
|
||||
tmp.forEach(function (pair) {
|
||||
if (pair.indexOf('access_token=') === 0) localStorage.token = pair.split('=')[1];
|
||||
});
|
||||
|
||||
var redirectTo = '/';
|
||||
let redirectTo = '/';
|
||||
if (localStorage.getItem('redirectToHash')) {
|
||||
redirectTo += localStorage.getItem('redirectToHash');
|
||||
localStorage.removeItem('redirectToHash');
|
||||
}
|
||||
window.location.href = redirectTo;
|
||||
|
||||
window.location.replace(redirectTo); // this removes us from history
|
||||
|
||||
</script>
|
||||
|
||||
@@ -5,10 +5,7 @@ set -eu
|
||||
echo "=> Create timezones.js"
|
||||
./scripts/createTimezones.cjs ./public/js/timezones.js
|
||||
|
||||
echo "=> Build theme.css for oidc views"
|
||||
./node_modules/.bin/sass --quiet --pkg-importer=node ./src/theme.scss ./public/theme.css
|
||||
|
||||
export VITE_CACHE_ID=$(date +%s)
|
||||
|
||||
echo "=> Build the dashboard apps"
|
||||
./node_modules/.bin/vite build
|
||||
npm run build
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import globals from 'globals';
|
||||
import js from '@eslint/js';
|
||||
import js from '@eslint/js';
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
ecmaVersion: 13,
|
||||
sourceType: 'module'
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "error"
|
||||
"semi": "error",
|
||||
"prefer-const": "error",
|
||||
"vue/no-reserved-component-names": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>File Manager</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,211 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="Application" ng-controller="MainController">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>Cloudron Dashboard</title>
|
||||
<meta name="description" content="Cloudron Dashboard">
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- contains all thing already using import statement -->
|
||||
<script type="module" src="./src/modules.js"></script>
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/js/jquery.min.js"></script>
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/slick.css"/>
|
||||
<link type="text/css" rel="stylesheet" href="/angular-ui-notification.css"/>
|
||||
<link type="text/css" rel="stylesheet" href="./src/theme.scss">
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Slick carousel -->
|
||||
<script type="text/javascript" src="/js/slick.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-route.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-cookies.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-animate.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-base64.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-md5.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-sanitize.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-slick.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-ui-notification.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-fittext.min.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/js/angular-translate.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-loader-static-files.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-storage-cookie.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-storage-local.min.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/js/clipboard.min.js"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/js/showdown-1.9.1.min.js"></script>
|
||||
|
||||
<!-- Anugular Multiselect https://github.com/sebastianha/angular-bootstrap-multiselect -->
|
||||
<script type="text/javascript" src="/js/angular-bootstrap-multiselect.js"></script>
|
||||
|
||||
<!-- timezone list -->
|
||||
<script type="text/javascript" src="/js/timezones.js?%VITE_CACHE_ID%"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<!-- for now we need this in a static non transformed index.js file -->
|
||||
<script> window.VITE_CACHE_ID = '%VITE_CACHE_ID%' </script>
|
||||
<script type="text/javascript" src="/js/index.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/js/client.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/js/utils.js?%VITE_CACHE_ID%"></script>
|
||||
|
||||
<script type="text/javascript" src="/views/app.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/apps.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/appstore.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/backups.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/branding.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/domains.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/email.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/emails-eventlog.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/emails.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/emails-queue.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/eventlog.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/network.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/notifications.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/profile.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/services.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/settings.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/support.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/system.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/user-directory.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/users.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/views/volumes.js?%VITE_CACHE_ID%"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script type="text/ng-template" id="notification.html">
|
||||
<div class="ui-notification">
|
||||
<h3 ng-show="title" ng-bind-html="title"></h3>
|
||||
<div class="message">
|
||||
<a href="{{action}}" ng-show="action" ng-bind-html="message"></a>
|
||||
<span ng-hide="action" ng-bind-html="message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> {{ 'main.offline' | tr }}</a>
|
||||
|
||||
<!-- Modal reboot server -->
|
||||
<div class="modal fade" id="rebootModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'main.rebootDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-bold">{{ 'main.rebootDialog.warning' | tr }}</p>
|
||||
<p>{{ 'main.rebootDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="reboot.submit()" ng-disabled="reboot.busy"><i class="fa fa-circle-notch fa-spin" ng-show="reboot.busy"></i> {{ 'main.rebootDialog.rebootAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mainContentContainer" class="animateMe ng-hide layout-root" ng-show="initialized">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img ng-src="{{ client.avatar }}" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="#/">{{ config.cloudronName || 'Cloudron' }}</a>
|
||||
</div>
|
||||
<!-- /.navbar-header -->
|
||||
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav navbar-right" ng-hide="hideNavBarActions">
|
||||
<li ng-show="user.isAtLeastOwner && (subscription.plan.id === 'free' || subscription.plan.id === 'expired')">
|
||||
<a ng-click="openSubscriptionSetup()" style="cursor: pointer">
|
||||
<span class="badge" ng-class="{'badge-danger': subscription.plan.id !== 'free', 'badge-success': subscription.plan.id === 'free' }">
|
||||
{{ subscription.plan.id === 'free' ? ('settings.appstoreAccount.subscriptionSetupAction' | tr) : ('settings.appstoreAccount.subscriptionReactivateAction' | tr) }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="!user.isAtLeastOwner && subscription.plan.id === 'expired'">
|
||||
<a>
|
||||
<span class="badge badge-danger">Subscription Expired</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-class="{ active: isActive('/apps')}" href="#/apps" ng-click="closeNavbar()"><i class="fa fa-grip fa-fw"></i> {{ 'apps.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore" ng-click="closeNavbar()"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ 'appstore.title' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastUserManager">
|
||||
<a ng-class="{ active: isActive('/users')}" href="#/users" ng-click="closeNavbar()"><i class="fa fa-users fa-fw"></i> {{ 'main.navbar.users' | tr }}</a>
|
||||
</li>
|
||||
<li ng-show="user.isAtLeastAdmin">
|
||||
<a href="#/notifications" ng-click="closeNavbar()">
|
||||
<i class="fas fa-bell" ng-show="notificationCount"></i>
|
||||
<i class="far fa-bell" ng-hide="notificationCount"></i>
|
||||
<span class="badge badge-danger" ng-show="notificationCount">{{ notificationCount === 100 ? '100+' : notificationCount }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.avatarUrl}}" style="width: 24px; height: 24px;"/> {{user.username}} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#/profile" ng-click="closeNavbar()"><i class="fa fa-user fa-fw"></i> {{ 'profile.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastMailManager" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/backups" ng-click="closeNavbar()"><i class="fa fa-archive fa-fw"></i> {{ 'backups.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/branding" ng-click="closeNavbar()"><i class="fa fa-passport fa-fw"></i> {{ 'branding.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/domains" ng-click="closeNavbar()"><i class="fa fa-globe fa-fw"></i> {{ 'domains.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastMailManager"><a href="#/email" ng-click="closeNavbar()"><i class="fa fa-envelope fa-fw"></i> {{ 'emails.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/eventlog" ng-click="closeNavbar()"><i class="fa fa-list-alt fa-fw"></i> {{ 'eventlog.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/network" ng-click="closeNavbar()"><i class="fas fa-network-wired fa-fw"></i> {{ 'network.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/services" ng-click="closeNavbar()"><i class="fa fa-cogs fa-fw"></i> {{ 'services.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/settings" ng-click="closeNavbar()"><i class="fa fa-wrench fa-fw"></i> {{ 'settings.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/user-directory" ng-click="closeNavbar()"><i class="fa fa-users-gear fa-fw"></i> {{ 'users.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/volumes" ng-click="closeNavbar()"><i class="fa fa-hdd fa-fw"></i> {{ 'volumes.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
|
||||
<li ng-show="user.isAtLeastOwner"><a href="#/support" ng-click="closeNavbar()"><i class="fa fa-comment fa-fw"></i> {{ 'support.title' | tr }}</a></li>
|
||||
<li ng-show="user.isAtLeastAdmin"><a href="#/system" ng-click="closeNavbar()"><i class="fa fa-chart-area fa-fw"></i> {{ 'system.title' | tr }}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out-alt fa-fw"></i> {{ 'main.logout' | tr }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div ng-view id="ng-view" class="layout-content"></div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted" ng-bind-html="config.footer | markdown2html"></span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= name %> Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" style="overflow: hidden; height: 100%;"></div>
|
||||
<script type="module" src="/src/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function injectMetaTags(templates) {
|
||||
let config;
|
||||
let template = fs.readFileSync(path.resolve('./src/meta-tags-header.html'), 'utf8');
|
||||
|
||||
return {
|
||||
name: 'inject-meta-tags',
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
|
||||
// remove all ejs content in dev mode
|
||||
if (!config.isProduction) template = template.replace(/<%[^]*?%>/g, '');
|
||||
},
|
||||
transformIndexHtml: {
|
||||
handler: function transform(html, ctx) {
|
||||
if (templates.indexOf(ctx.filename) === -1) return html;
|
||||
|
||||
html = html.replace(/<head(.*)>/i, `$&\n${template}`);
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default injectMetaTags;
|
||||
@@ -1,17 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Logs</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= name %> OpenID Error</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.errorMessage = `<%- errorMessage %>`;
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/oidcerror.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= name %> OpenID Access Denied</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.submitUrl = '<%- submitUrl %>';
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/oidcinteractionabort.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Authorize <%= name %></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<form id="submitForm" method="post" action="<%- submitUrl %>">
|
||||
<!-- <button type="submit"></button> -->
|
||||
</form>
|
||||
|
||||
<script>
|
||||
|
||||
document.getElementById('submitForm').submit();
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= name %> Login</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.note = '<%- note %>';
|
||||
window.cloudron.submitUrl = '<%- submitUrl %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,31 +3,30 @@
|
||||
"scripts": {
|
||||
"update-translations": "curl https://translate.cloudron.io/api/components/cloudron/dashboard/file/ -o lang.zip && unzip -jo lang.zip -d ./public/translation/ && rm lang.zip",
|
||||
"dev": "vite --strictPort --port 4000",
|
||||
"build": "vite build"
|
||||
"build": "rm -rf ./dist/* && vite build --config ./vite.config.mjs && vite build --config ./vite.proxyauth.config.mjs"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@fontsource/noto-sans": "^5.1.0",
|
||||
"@fortawesome/fontawesome-free": "^6.7.1",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@cloudron/pankow": "^3.5.1",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"anser": "^2.3.0",
|
||||
"bootstrap-sass": "^3.4.3",
|
||||
"chart.js": "^4.4.7",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"filesize": "^10.1.6",
|
||||
"jquery": "^3.7.1",
|
||||
"marked": "^15.0.3",
|
||||
"anser": "^2.3.2",
|
||||
"async": "^3.2.6",
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"marked": "^16.4.0",
|
||||
"moment": "^2.30.1",
|
||||
"pankow": "^2.4.2",
|
||||
"pankow-viewers": "^1.0.11",
|
||||
"sass": "^1.82.0",
|
||||
"vite": "^6.0.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^10.0.5",
|
||||
"vue-router": "^4.5.0"
|
||||
"moment-timezone": "^0.6.0",
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-router": "^4.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,165 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= name %> Password Reset</title>
|
||||
|
||||
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title>Cloudron Password Reset</title>
|
||||
<meta name="description" content="Cloudron Password Reset">
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.footer = `<%- footer -%>`;
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
</script>
|
||||
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
</head>
|
||||
|
||||
<!-- contains all thing already using import statement -->
|
||||
<script type="module" src="./src/modules.js"></script>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/passwordreset.js"></script>
|
||||
</body>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="./src/theme.scss">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script type="text/javascript" src="/js/jquery.min.js"></script>
|
||||
|
||||
<!-- async -->
|
||||
<script type="text/javascript" src="/js/async-3.2.0.min.js"></script>
|
||||
|
||||
<!-- Showdown (markdown converter) -->
|
||||
<script type="text/javascript" src="/js/showdown-1.9.1.min.js"></script>
|
||||
|
||||
<!-- Angularjs scripts -->
|
||||
<script type="text/javascript" src="/js/angular.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-loader.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-cookies.min.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script type="text/javascript" src="/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Angular translate https://angular-translate.github.io/ -->
|
||||
<script type="text/javascript" src="/js/angular-translate.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-loader-static-files.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-storage-cookie.min.js"></script>
|
||||
<script type="text/javascript" src="/js/angular-translate-storage-local.min.js"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script type="text/javascript" src="/js/passwordreset.js?%VITE_CACHE_ID%"></script>
|
||||
<script type="text/javascript" src="/js/utils.js?%VITE_CACHE_ID%"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-app="Application" ng-controller="PasswordResetController">
|
||||
|
||||
<div class="layout-root ng-cloak" ng-show="initialized">
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'passwordReset'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.title' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="passwordResetForm" ng-submit="onPasswordReset()">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPasswordResetIdentifier">{{ 'passwordReset.usernameOrEmail' | tr }}</label>
|
||||
<input type="text" class="form-control" id="inputPasswordResetIdentifier" name="passwordResetIdentifier" ng-model="passwordResetIdentifier" ng-disabled="busy" autofocus required>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'passwordResetDone'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2 ng-hide="error">{{ 'passwordReset.emailSent.title' | tr }}</h2>
|
||||
<h4 ng-show="error" class="has-error">{{ error }}</h4>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'newPassword'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.newPassword.title' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error" ng-show="error">{{ error }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="newPasswordForm" ng-submit="onNewPassword()">
|
||||
<input type="password" style="display: none;"/>
|
||||
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid }">
|
||||
<label class="control-label" for="inputNewPassword">{{ 'passwordReset.newPassword.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">
|
||||
<small ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">{{ 'passwordReset.newPassword.errorLength' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputNewPassword" ng-model="newPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" autofocus required password-reveal>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputNewPasswordRepeat">{{ 'passwordReset.newPassword.passwordRepeat' | tr }}</label>
|
||||
<div class="control-label" ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">
|
||||
<small ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">{{ 'passwordReset.newPassword.errorMismatch' | tr }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputNewPasswordRepeat" ng-model="newPasswordRepeat" name="newPasswordRepeat" required password-reveal>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPasswordResetTotpToken">{{ 'login.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" name="passwordResetTotpToken" id="inputPasswordResetTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
|
||||
</div>
|
||||
<div class="card-form-bottom-bar">
|
||||
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
|
||||
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-content" ng-show="mode === 'newPasswordDone'">
|
||||
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" style="margin-top: -84px" src="/api/v1/cloudron/avatar"/>
|
||||
<br/>
|
||||
<h2>{{ 'passwordReset.success.title' | tr }}</h2>
|
||||
<br/>
|
||||
<a href="/" class="btn btn-primary">{{ 'passwordReset.success.openDashboardAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted" ng-bind-html="branding.footer | markdown2html"></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= name %> Login</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = {};
|
||||
window.cloudron.name = '<%= name %>';
|
||||
window.cloudron.iconUrl = '<%- iconUrl %>';
|
||||
window.cloudron.loginUrl = '<%- loginUrl %>';
|
||||
window.cloudron.language = `<%= language %>`;
|
||||
window.cloudron.apiOrigin = `<%= apiOrigin %>`;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/proxyauth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
|
||||
* @author Alex_Crack
|
||||
* @version v0.3.6
|
||||
* @link https://github.com/alexcrack/angular-ui-notification
|
||||
* @license MIT
|
||||
*/
|
||||
.ui-notification
|
||||
{
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
|
||||
width: 300px;
|
||||
|
||||
-webkit-transition: all ease .5s;
|
||||
-o-transition: all ease .5s;
|
||||
transition: all ease .5s;
|
||||
|
||||
color: #fff;
|
||||
border-radius: 0;
|
||||
background: #337ab7;
|
||||
box-shadow: 5px 5px 10px rgba(0, 0, 0, .3);
|
||||
}
|
||||
.ui-notification.clickable
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-notification.clickable:hover
|
||||
{
|
||||
opacity: .7;
|
||||
}
|
||||
.ui-notification.killed
|
||||
{
|
||||
-webkit-transition: opacity ease 1s;
|
||||
-o-transition: opacity ease 1s;
|
||||
transition: opacity ease 1s;
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
.ui-notification > h3
|
||||
{
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
display: block;
|
||||
|
||||
margin: 10px 10px 0 10px;
|
||||
padding: 0 0 5px 0;
|
||||
|
||||
text-align: left;
|
||||
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .3);
|
||||
}
|
||||
.ui-notification a
|
||||
{
|
||||
color: #fff;
|
||||
}
|
||||
.ui-notification a:hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ui-notification > .message
|
||||
{
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
.ui-notification.warning
|
||||
{
|
||||
color: #fff;
|
||||
background: #f0ad4e;
|
||||
}
|
||||
.ui-notification.error
|
||||
{
|
||||
color: #fff;
|
||||
background: #d9534f;
|
||||
}
|
||||
.ui-notification.success
|
||||
{
|
||||
color: #fff;
|
||||
background: #5cb85c;
|
||||
}
|
||||
.ui-notification.info
|
||||
{
|
||||
color: #fff;
|
||||
background: #5bc0de;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="546.13336"
|
||||
height="546.13336"
|
||||
viewBox="0 0 512.00001 512.00001"
|
||||
id="svg4519"
|
||||
version="1.1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="app_down.svg"
|
||||
inkscape:export-filename="/home/nebulon/Cloudron/Assets/logo_115.png"
|
||||
inkscape:export-xdpi="20.214842"
|
||||
inkscape:export-ydpi="20.214842"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||
id="defs4521" /><sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="250.01276"
|
||||
inkscape:cy="238.90108"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="g4496"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1014"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" /><metadata
|
||||
id="metadata4524"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-540.36216)"><g
|
||||
id="g4467"
|
||||
transform="matrix(20.50952,0,0,20.859456,-526.58031,-94.042799)"><g
|
||||
inkscape:export-ydpi="67.349998"
|
||||
inkscape:export-xdpi="67.349998"
|
||||
transform="matrix(0.59473169,0,0,0.59473169,31.04719,102.48374)"
|
||||
id="g4382"><g
|
||||
id="g4496"><path
|
||||
sodipodi:type="star"
|
||||
style="opacity:1;fill:#7c7c7c;fill-opacity:1;stroke:none;stroke-width:1.10000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path4162"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="12.46875"
|
||||
sodipodi:cy="-99.893143"
|
||||
sodipodi:r1="19.266006"
|
||||
sodipodi:r2="16.307295"
|
||||
sodipodi:arg1="-0.52224059"
|
||||
sodipodi:arg2="0.0013581913"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0.12490573"
|
||||
inkscape:randomized="0"
|
||||
d="m 29.166669,-109.50348 c 1.200386,2.08567 1.17988,17.183595 -0.02617,19.265993 -1.206046,2.082397 -14.291486,9.613601 -16.697919,9.610333 -2.406432,-0.0033 -15.4713664,-7.56999 -16.671752,-9.655655 -1.2003857,-2.085666 -1.1798799,-17.183591 0.026167,-19.265991 1.2060467,-2.0824 14.2914862,-9.6136 16.6979192,-9.61033 2.406432,0.003 15.471366,7.56999 16.671752,9.65565 z"
|
||||
transform="rotate(-30,10.993604,-99.259973)"
|
||||
inkscape:export-xdpi="67.349998"
|
||||
inkscape:export-ydpi="67.349998" /><rect
|
||||
inkscape:transform-center-x="0.40624986"
|
||||
ry="2.4183984"
|
||||
y="-104.9176"
|
||||
x="2.2207832"
|
||||
height="8.7434387"
|
||||
width="8.7434387"
|
||||
id="rect4168-1-1"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.29999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /><g
|
||||
transform="translate(0,0.14463441)"
|
||||
id="g4491"><rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.29999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4168-1-1-7"
|
||||
width="8.7434387"
|
||||
height="8.7434387"
|
||||
x="9.0890703"
|
||||
y="-98.734459"
|
||||
ry="2.4183986" /><rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.29999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4168-1-1-7-2"
|
||||
width="8.7434387"
|
||||
height="8.7434387"
|
||||
x="9.0890703"
|
||||
y="-111.39002"
|
||||
ry="2.4183986" /></g><ellipse
|
||||
style="fill:#ca3636;stroke-width:1.36965;fill-opacity:1"
|
||||
id="path1"
|
||||
cx="23.01207"
|
||||
cy="-89.902901"
|
||||
rx="9.6073742"
|
||||
ry="9.4462013" /><path
|
||||
d="m 26.439777,-95.584412 c 0.161271,-0.368194 0.04101,-0.798203 -0.289743,-1.034712 -0.330744,-0.236504 -0.781758,-0.215002 -1.090634,0.04838 l -6.997553,6.020141 c -0.273341,0.236508 -0.371745,0.615454 -0.243273,0.948711 0.12847,0.333256 0.45648,0.559013 0.820025,0.559013 h 3.047762 l -2.101999,4.821487 c -0.161272,0.368197 -0.041,0.798206 0.289742,1.034712 0.330744,0.236506 0.781758,0.215005 1.090634,-0.04837 l 6.997553,-6.020141 c 0.273342,-0.236505 0.371745,-0.615454 0.243274,-0.948711 -0.12847,-0.333256 -0.453747,-0.556325 -0.820026,-0.556325 h -3.047762 z"
|
||||
id="path1-7"
|
||||
style="fill:#ffffff;stroke-width:0.027104" /></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="546.13336"
|
||||
height="546.13336"
|
||||
viewBox="0 0 512.00001 512.00001"
|
||||
id="svg4519"
|
||||
version="1.1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="not_found.svg"
|
||||
inkscape:export-filename="/home/nebulon/Cloudron/Assets/logo_115.png"
|
||||
inkscape:export-xdpi="20.214842"
|
||||
inkscape:export-ydpi="20.214842"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||
id="defs4521" /><sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="250.01276"
|
||||
inkscape:cy="239.91123"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="g4496"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1014"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" /><metadata
|
||||
id="metadata4524"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-540.36216)"><g
|
||||
id="g4467"
|
||||
transform="matrix(20.50952,0,0,20.859456,-526.58031,-94.042799)"><g
|
||||
inkscape:export-ydpi="67.349998"
|
||||
inkscape:export-xdpi="67.349998"
|
||||
transform="matrix(0.59473169,0,0,0.59473169,31.04719,102.48374)"
|
||||
id="g4382"><g
|
||||
id="g4496"><path
|
||||
sodipodi:type="star"
|
||||
style="opacity:1;fill:#7c7c7c;fill-opacity:1;stroke:none;stroke-width:1.10000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path4162"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="12.46875"
|
||||
sodipodi:cy="-99.893143"
|
||||
sodipodi:r1="19.266006"
|
||||
sodipodi:r2="16.307295"
|
||||
sodipodi:arg1="-0.52224059"
|
||||
sodipodi:arg2="0.0013581913"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0.12490573"
|
||||
inkscape:randomized="0"
|
||||
d="m 29.166669,-109.50348 c 1.200386,2.08567 1.17988,17.183595 -0.02617,19.265993 -1.206046,2.082397 -14.291486,9.613601 -16.697919,9.610333 -2.406432,-0.0033 -15.4713664,-7.56999 -16.671752,-9.655655 -1.2003857,-2.085666 -1.1798799,-17.183591 0.026167,-19.265991 1.2060467,-2.0824 14.2914862,-9.6136 16.6979192,-9.61033 2.406432,0.003 15.471366,7.56999 16.671752,9.65565 z"
|
||||
transform="rotate(-30,10.993604,-99.259973)"
|
||||
inkscape:export-xdpi="67.349998"
|
||||
inkscape:export-ydpi="67.349998" /><rect
|
||||
inkscape:transform-center-x="0.40624986"
|
||||
ry="2.4183984"
|
||||
y="-104.9176"
|
||||
x="2.2207832"
|
||||
height="8.7434387"
|
||||
width="8.7434387"
|
||||
id="rect4168-1-1"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.29999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /><g
|
||||
transform="translate(0,0.14463441)"
|
||||
id="g4491"><rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.29999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4168-1-1-7"
|
||||
width="8.7434387"
|
||||
height="8.7434387"
|
||||
x="9.0890703"
|
||||
y="-98.734459"
|
||||
ry="2.4183986" /><rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.29999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4168-1-1-7-2"
|
||||
width="8.7434387"
|
||||
height="8.7434387"
|
||||
x="9.0890703"
|
||||
y="-111.39002"
|
||||
ry="2.4183986" /></g><ellipse
|
||||
style="fill:#1a76bf;stroke-width:1.36965;fill-opacity:1"
|
||||
id="path1"
|
||||
cx="23.01207"
|
||||
cy="-89.902901"
|
||||
rx="9.6073742"
|
||||
ry="9.4462013" /><path
|
||||
d="m 22.28174,-94.476029 h -3.651649 c -0.403963,0 -0.730329,0.320891 -0.730329,0.718076 v 1.436157 c 0,0.397187 0.326366,0.718079 0.730329,0.718079 h 8.613327 c 0.09585,0 0.189429,-0.03815 0.257897,-0.105469 l 1.095495,-1.077115 c 0.141502,-0.139127 0.141502,-0.368016 0,-0.507142 l -1.095495,-1.07712 c -0.06847,-0.06732 -0.162041,-0.105466 -0.257897,-0.105466 h -3.501017 c 0,-0.397187 -0.326367,-0.718075 -0.730331,-0.718075 -0.403964,0 -0.73033,0.320888 -0.73033,0.718075 z m 5.842639,5.026544 c 0,-0.397185 -0.326366,-0.718078 -0.730331,-0.718078 h -3.651647 v -0.718076 H 22.28174 v 0.718076 h -3.501017 c -0.09585,0 -0.18943,0.03815 -0.257898,0.105468 l -1.095496,1.077117 c -0.141501,0.139127 -0.141501,0.368014 0,0.507141 l 1.095496,1.077118 c 0.06847,0.06732 0.162042,0.105467 0.257898,0.105467 h 8.613325 c 0.403965,0 0.730331,-0.320892 0.730331,-0.718078 z m -4.381978,5.026544 v -2.154233 H 22.28174 v 2.154233 c 0,0.397187 0.326366,0.718078 0.73033,0.718078 0.403964,0 0.730331,-0.320891 0.730331,-0.718078 z"
|
||||
id="path1-3"
|
||||
style="fill:#ffffff;stroke-width:0.0226306" /></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -1,99 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, redirectIfNeeded */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.controller('SetupController', ['$scope', 'Client', function ($scope, Client) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
const search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.view = '';
|
||||
$scope.initialized = false;
|
||||
$scope.setupToken = '';
|
||||
$scope.firstTimeLoginUrl = '';
|
||||
|
||||
$scope.owner = {
|
||||
error: null,
|
||||
busy: false,
|
||||
|
||||
email: '',
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.owner.busy = true;
|
||||
$scope.owner.error = null;
|
||||
|
||||
var data = {
|
||||
username: $scope.owner.username,
|
||||
password: $scope.owner.password,
|
||||
email: $scope.owner.email,
|
||||
displayName: $scope.owner.displayName,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.createAdmin(data, function (error, autoLoginToken) {
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.owner.busy = false;
|
||||
|
||||
if (error.message === 'Invalid email') {
|
||||
$scope.owner.error = { email: error.message };
|
||||
$scope.owner.email = '';
|
||||
$scope.ownerForm.email.$setPristine();
|
||||
setTimeout(function () { $('#inputEmail').focus(); }, 200);
|
||||
} else {
|
||||
$scope.owner.error = { username: error.message };
|
||||
$scope.owner.username = '';
|
||||
$scope.ownerForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
}
|
||||
return;
|
||||
} else if (error) {
|
||||
$scope.owner.busy = false;
|
||||
console.error('Internal error', error);
|
||||
$scope.owner.error = { generic: error.message };
|
||||
return;
|
||||
}
|
||||
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = autoLoginToken;
|
||||
|
||||
$scope.firstTimeLoginUrl = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
|
||||
setView('finished');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function setView(view) {
|
||||
if (view === 'finished') {
|
||||
$scope.view = 'finished';
|
||||
} else {
|
||||
$scope.view = 'owner';
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status, 'activation')) return; // redirected to some other view...
|
||||
|
||||
setView(search.view);
|
||||
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.initialized = true;
|
||||
|
||||
// Ensure we have a good autofocus
|
||||
setTimeout(function () {
|
||||
$(document).find("[autofocus]:first").focus();
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}]);
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(R,B){'use strict';function Da(a,b,c){if(!a)throw Ma("areq",b||"?",c||"required");return a}function Ea(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;Y(a)&&(a=a.join(" "));Y(b)&&(b=b.join(" "));return a+" "+b}function Na(a){var b={};a&&(a.to||a.from)&&(b.to=a.to,b.from=a.from);return b}function Z(a,b,c){var d="";a=Y(a)?a:a&&G(a)&&a.length?a.split(/\s+/):[];s(a,function(a,l){a&&0<a.length&&(d+=0<l?" ":"",d+=c?b+a:a+b)});return d}function Oa(a){if(a instanceof F)switch(a.length){case 0:return a;
|
||||
case 1:if(1===a[0].nodeType)return a;break;default:return F(ta(a))}if(1===a.nodeType)return F(a)}function ta(a){if(!a[0])return a;for(var b=0;b<a.length;b++){var c=a[b];if(1==c.nodeType)return c}}function Pa(a,b,c){s(b,function(b){a.addClass(b,c)})}function Qa(a,b,c){s(b,function(b){a.removeClass(b,c)})}function V(a){return function(b,c){c.addClass&&(Pa(a,b,c.addClass),c.addClass=null);c.removeClass&&(Qa(a,b,c.removeClass),c.removeClass=null)}}function oa(a){a=a||{};if(!a.$$prepared){var b=a.domOperation||
|
||||
P;a.domOperation=function(){a.$$domOperationFired=!0;b();b=P};a.$$prepared=!0}return a}function ha(a,b){Fa(a,b);Ga(a,b)}function Fa(a,b){b.from&&(a.css(b.from),b.from=null)}function Ga(a,b){b.to&&(a.css(b.to),b.to=null)}function W(a,b,c){var d=b.options||{};c=c.options||{};var e=(d.addClass||"")+" "+(c.addClass||""),l=(d.removeClass||"")+" "+(c.removeClass||"");a=Ra(a.attr("class"),e,l);c.preparationClasses&&(d.preparationClasses=$(c.preparationClasses,d.preparationClasses),delete c.preparationClasses);
|
||||
e=d.domOperation!==P?d.domOperation:null;ua(d,c);e&&(d.domOperation=e);d.addClass=a.addClass?a.addClass:null;d.removeClass=a.removeClass?a.removeClass:null;b.addClass=d.addClass;b.removeClass=d.removeClass;return d}function Ra(a,b,c){function d(a){G(a)&&(a=a.split(" "));var b={};s(a,function(a){a.length&&(b[a]=!0)});return b}var e={};a=d(a);b=d(b);s(b,function(a,b){e[b]=1});c=d(c);s(c,function(a,b){e[b]=1===e[b]?null:-1});var l={addClass:"",removeClass:""};s(e,function(b,c){var d,e;1===b?(d="addClass",
|
||||
e=!a[c]||a[c+"-remove"]):-1===b&&(d="removeClass",e=a[c]||a[c+"-add"]);e&&(l[d].length&&(l[d]+=" "),l[d]+=c)});return l}function y(a){return a instanceof F?a[0]:a}function Sa(a,b,c){var d="";b&&(d=Z(b,"ng-",!0));c.addClass&&(d=$(d,Z(c.addClass,"-add")));c.removeClass&&(d=$(d,Z(c.removeClass,"-remove")));d.length&&(c.preparationClasses=d,a.addClass(d))}function pa(a,b){var c=b?"-"+b+"s":"";la(a,[ma,c]);return[ma,c]}function va(a,b){var c=b?"paused":"",d=aa+"PlayState";la(a,[d,c]);return[d,c]}function la(a,
|
||||
b){a.style[b[0]]=b[1]}function $(a,b){return a?b?a+" "+b:a:b}function Ha(a,b,c){var d=Object.create(null),e=a.getComputedStyle(b)||{};s(c,function(a,b){var c=e[a];if(c){var g=c.charAt(0);if("-"===g||"+"===g||0<=g)c=Ta(c);0===c&&(c=null);d[b]=c}});return d}function Ta(a){var b=0;a=a.split(/\s*,\s*/);s(a,function(a){"s"==a.charAt(a.length-1)&&(a=a.substring(0,a.length-1));a=parseFloat(a)||0;b=b?Math.max(a,b):a});return b}function wa(a){return 0===a||null!=a}function Ia(a,b){var c=S,d=a+"s";b?c+="Duration":
|
||||
d+=" linear all";return[c,d]}function Ja(){var a=Object.create(null);return{flush:function(){a=Object.create(null)},count:function(b){return(b=a[b])?b.total:0},get:function(b){return(b=a[b])&&b.value},put:function(b,c){a[b]?a[b].total++:a[b]={total:1,value:c}}}}function Ka(a,b,c){s(c,function(c){a[c]=xa(a[c])?a[c]:b.style.getPropertyValue(c)})}var S,ya,aa,za;void 0===R.ontransitionend&&void 0!==R.onwebkittransitionend?(S="WebkitTransition",ya="webkitTransitionEnd transitionend"):(S="transition",ya=
|
||||
"transitionend");void 0===R.onanimationend&&void 0!==R.onwebkitanimationend?(aa="WebkitAnimation",za="webkitAnimationEnd animationend"):(aa="animation",za="animationend");var qa=aa+"Delay",Aa=aa+"Duration",ma=S+"Delay",La=S+"Duration",Ma=B.$$minErr("ng"),Ua={transitionDuration:La,transitionDelay:ma,transitionProperty:S+"Property",animationDuration:Aa,animationDelay:qa,animationIterationCount:aa+"IterationCount"},Va={transitionDuration:La,transitionDelay:ma,animationDuration:Aa,animationDelay:qa},
|
||||
Ba,ua,s,Y,xa,ea,Ca,ba,G,J,F,P;B.module("ngAnimate",[],function(){P=B.noop;Ba=B.copy;ua=B.extend;F=B.element;s=B.forEach;Y=B.isArray;G=B.isString;ba=B.isObject;J=B.isUndefined;xa=B.isDefined;Ca=B.isFunction;ea=B.isElement}).directive("ngAnimateSwap",["$animate","$rootScope",function(a,b){return{restrict:"A",transclude:"element",terminal:!0,priority:600,link:function(b,d,e,l,n){var I,g;b.$watchCollection(e.ngAnimateSwap||e["for"],function(e){I&&a.leave(I);g&&(g.$destroy(),g=null);if(e||0===e)g=b.$new(),
|
||||
n(g,function(b){I=b;a.enter(b,null,d)})})}}}]).directive("ngAnimateChildren",["$interpolate",function(a){return{link:function(b,c,d){function e(a){c.data("$$ngAnimateChildren","on"===a||"true"===a)}var l=d.ngAnimateChildren;G(l)&&0===l.length?c.data("$$ngAnimateChildren",!0):(e(a(l)(b)),d.$observe("ngAnimateChildren",e))}}}]).factory("$$rAFScheduler",["$$rAF",function(a){function b(a){d=d.concat(a);c()}function c(){if(d.length){for(var b=d.shift(),n=0;n<b.length;n++)b[n]();e||a(function(){e||c()})}}
|
||||
var d,e;d=b.queue=[];b.waitUntilQuiet=function(b){e&&e();e=a(function(){e=null;b();c()})};return b}]).provider("$$animateQueue",["$animateProvider",function(a){function b(a){if(!a)return null;a=a.split(" ");var b=Object.create(null);s(a,function(a){b[a]=!0});return b}function c(a,c){if(a&&c){var d=b(c);return a.split(" ").some(function(a){return d[a]})}}function d(a,b,c,d){return l[a].some(function(a){return a(b,c,d)})}function e(a,b){var c=0<(a.addClass||"").length,d=0<(a.removeClass||"").length;
|
||||
return b?c&&d:c||d}var l=this.rules={skip:[],cancel:[],join:[]};l.join.push(function(a,b,c){return!b.structural&&e(b)});l.skip.push(function(a,b,c){return!b.structural&&!e(b)});l.skip.push(function(a,b,c){return"leave"==c.event&&b.structural});l.skip.push(function(a,b,c){return c.structural&&2===c.state&&!b.structural});l.cancel.push(function(a,b,c){return c.structural&&b.structural});l.cancel.push(function(a,b,c){return 2===c.state&&b.structural});l.cancel.push(function(a,b,d){if(d.structural)return!1;
|
||||
a=b.addClass;b=b.removeClass;var e=d.addClass;d=d.removeClass;return J(a)&&J(b)||J(e)&&J(d)?!1:c(a,d)||c(b,e)});this.$get=["$$rAF","$rootScope","$rootElement","$document","$$HashMap","$$animation","$$AnimateRunner","$templateRequest","$$jqLite","$$forceReflow",function(b,c,g,l,C,Wa,Q,t,H,T){function O(){var a=!1;return function(b){a?b():c.$$postDigest(function(){a=!0;b()})}}function x(a,b,c){var f=y(b),d=y(a),N=[];(a=h[c])&&s(a,function(a){w.call(a.node,f)?N.push(a.callback):"leave"===c&&w.call(a.node,
|
||||
d)&&N.push(a.callback)});return N}function r(a,b,c){var f=ta(b);return a.filter(function(a){return!(a.node===f&&(!c||a.callback===c))})}function p(a,h,v){function r(c,f,d,h){sa(function(){var c=x(T,a,f);c.length?b(function(){s(c,function(b){b(a,d,h)});"close"!==d||a[0].parentNode||ra.off(a)}):"close"!==d||a[0].parentNode||ra.off(a)});c.progress(f,d,h)}function k(b){var c=a,f=m;f.preparationClasses&&(c.removeClass(f.preparationClasses),f.preparationClasses=null);f.activeClasses&&(c.removeClass(f.activeClasses),
|
||||
f.activeClasses=null);E(a,m);ha(a,m);m.domOperation();A.complete(!b)}var m=Ba(v),p,T;if(a=Oa(a))p=y(a),T=a.parent();var m=oa(m),A=new Q,sa=O();Y(m.addClass)&&(m.addClass=m.addClass.join(" "));m.addClass&&!G(m.addClass)&&(m.addClass=null);Y(m.removeClass)&&(m.removeClass=m.removeClass.join(" "));m.removeClass&&!G(m.removeClass)&&(m.removeClass=null);m.from&&!ba(m.from)&&(m.from=null);m.to&&!ba(m.to)&&(m.to=null);if(!p)return k(),A;v=[p.className,m.addClass,m.removeClass].join(" ");if(!Xa(v))return k(),
|
||||
A;var g=0<=["enter","move","leave"].indexOf(h),w=l[0].hidden,t=!f||w||N.get(p);v=!t&&z.get(p)||{};var H=!!v.state;t||H&&1==v.state||(t=!M(a,T,h));if(t)return w&&r(A,h,"start"),k(),w&&r(A,h,"close"),A;g&&K(a);w={structural:g,element:a,event:h,addClass:m.addClass,removeClass:m.removeClass,close:k,options:m,runner:A};if(H){if(d("skip",a,w,v)){if(2===v.state)return k(),A;W(a,v,w);return v.runner}if(d("cancel",a,w,v))if(2===v.state)v.runner.end();else if(v.structural)v.close();else return W(a,v,w),v.runner;
|
||||
else if(d("join",a,w,v))if(2===v.state)W(a,w,{});else return Sa(a,g?h:null,m),h=w.event=v.event,m=W(a,v,w),v.runner}else W(a,w,{});(H=w.structural)||(H="animate"===w.event&&0<Object.keys(w.options.to||{}).length||e(w));if(!H)return k(),ka(a),A;var C=(v.counter||0)+1;w.counter=C;L(a,1,w);c.$$postDigest(function(){var b=z.get(p),c=!b,b=b||{},f=0<(a.parent()||[]).length&&("animate"===b.event||b.structural||e(b));if(c||b.counter!==C||!f){c&&(E(a,m),ha(a,m));if(c||g&&b.event!==h)m.domOperation(),A.end();
|
||||
f||ka(a)}else h=!b.structural&&e(b,!0)?"setClass":b.event,L(a,2),b=Wa(a,h,b.options),A.setHost(b),r(A,h,"start",{}),b.done(function(b){k(!b);(b=z.get(p))&&b.counter===C&&ka(y(a));r(A,h,"close",{})})});return A}function K(a){a=y(a).querySelectorAll("[data-ng-animate]");s(a,function(a){var b=parseInt(a.getAttribute("data-ng-animate")),c=z.get(a);if(c)switch(b){case 2:c.runner.end();case 1:z.remove(a)}})}function ka(a){a=y(a);a.removeAttribute("data-ng-animate");z.remove(a)}function k(a,b){return y(a)===
|
||||
y(b)}function M(a,b,c){c=F(l[0].body);var f=k(a,c)||"HTML"===a[0].nodeName,d=k(a,g),h=!1,r,e=N.get(y(a));(a=F.data(a[0],"$ngAnimatePin"))&&(b=a);for(b=y(b);b;){d||(d=k(b,g));if(1!==b.nodeType)break;a=z.get(b)||{};if(!h){var p=N.get(b);if(!0===p&&!1!==e){e=!0;break}else!1===p&&(e=!1);h=a.structural}if(J(r)||!0===r)a=F.data(b,"$$ngAnimateChildren"),xa(a)&&(r=a);if(h&&!1===r)break;f||(f=k(b,c));if(f&&d)break;if(!d&&(a=F.data(b,"$ngAnimatePin"))){b=y(a);continue}b=b.parentNode}return(!h||r)&&!0!==e&&
|
||||
d&&f}function L(a,b,c){c=c||{};c.state=b;a=y(a);a.setAttribute("data-ng-animate",b);c=(b=z.get(a))?ua(b,c):c;z.put(a,c)}var z=new C,N=new C,f=null,A=c.$watch(function(){return 0===t.totalPendingRequests},function(a){a&&(A(),c.$$postDigest(function(){c.$$postDigest(function(){null===f&&(f=!0)})}))}),h=Object.create(null),sa=a.classNameFilter(),Xa=sa?function(a){return sa.test(a)}:function(){return!0},E=V(H),w=R.Node.prototype.contains||function(a){return this===a||!!(this.compareDocumentPosition(a)&
|
||||
16)},ra={on:function(a,b,c){var f=ta(b);h[a]=h[a]||[];h[a].push({node:f,callback:c});F(b).on("$destroy",function(){z.get(f)||ra.off(a,b,c)})},off:function(a,b,c){if(1!==arguments.length||G(arguments[0])){var f=h[a];f&&(h[a]=1===arguments.length?null:r(f,b,c))}else for(f in b=arguments[0],h)h[f]=r(h[f],b)},pin:function(a,b){Da(ea(a),"element","not an element");Da(ea(b),"parentElement","not an element");a.data("$ngAnimatePin",b)},push:function(a,b,c,f){c=c||{};c.domOperation=f;return p(a,b,c)},enabled:function(a,
|
||||
b){var c=arguments.length;if(0===c)b=!!f;else if(ea(a)){var d=y(a);1===c?b=!N.get(d):N.put(d,!b)}else b=f=!!a;return b}};return ra}]}]).provider("$$animation",["$animateProvider",function(a){var b=this.drivers=[];this.$get=["$$jqLite","$rootScope","$injector","$$AnimateRunner","$$HashMap","$$rAFScheduler",function(a,d,e,l,n,I){function g(a){function b(a){if(a.processed)return a;a.processed=!0;var d=a.domNode,p=d.parentNode;e.put(d,a);for(var K;p;){if(K=e.get(p)){K.processed||(K=b(K));break}p=p.parentNode}(K||
|
||||
c).children.push(a);return a}var c={children:[]},d,e=new n;for(d=0;d<a.length;d++){var g=a[d];e.put(g.domNode,a[d]={domNode:g.domNode,fn:g.fn,children:[]})}for(d=0;d<a.length;d++)b(a[d]);return function(a){var b=[],c=[],d;for(d=0;d<a.children.length;d++)c.push(a.children[d]);a=c.length;var e=0,k=[];for(d=0;d<c.length;d++){var g=c[d];0>=a&&(a=e,e=0,b.push(k),k=[]);k.push(g.fn);g.children.forEach(function(a){e++;c.push(a)});a--}k.length&&b.push(k);return b}(c)}var u=[],C=V(a);return function(n,Q,t){function H(a){a=
|
||||
a.hasAttribute("ng-animate-ref")?[a]:a.querySelectorAll("[ng-animate-ref]");var b=[];s(a,function(a){var c=a.getAttribute("ng-animate-ref");c&&c.length&&b.push(a)});return b}function T(a){var b=[],c={};s(a,function(a,d){var h=y(a.element),e=0<=["enter","move"].indexOf(a.event),h=a.structural?H(h):[];if(h.length){var k=e?"to":"from";s(h,function(a){var b=a.getAttribute("ng-animate-ref");c[b]=c[b]||{};c[b][k]={animationID:d,element:F(a)}})}else b.push(a)});var d={},e={};s(c,function(c,k){var r=c.from,
|
||||
p=c.to;if(r&&p){var z=a[r.animationID],g=a[p.animationID],A=r.animationID.toString();if(!e[A]){var n=e[A]={structural:!0,beforeStart:function(){z.beforeStart();g.beforeStart()},close:function(){z.close();g.close()},classes:O(z.classes,g.classes),from:z,to:g,anchors:[]};n.classes.length?b.push(n):(b.push(z),b.push(g))}e[A].anchors.push({out:r.element,"in":p.element})}else r=r?r.animationID:p.animationID,p=r.toString(),d[p]||(d[p]=!0,b.push(a[r]))});return b}function O(a,b){a=a.split(" ");b=b.split(" ");
|
||||
for(var c=[],d=0;d<a.length;d++){var e=a[d];if("ng-"!==e.substring(0,3))for(var r=0;r<b.length;r++)if(e===b[r]){c.push(e);break}}return c.join(" ")}function x(a){for(var c=b.length-1;0<=c;c--){var d=e.get(b[c])(a);if(d)return d}}function r(a,b){function c(a){(a=a.data("$$animationRunner"))&&a.setHost(b)}a.from&&a.to?(c(a.from.element),c(a.to.element)):c(a.element)}function p(){var a=n.data("$$animationRunner");!a||"leave"===Q&&t.$$domOperationFired||a.end()}function K(b){n.off("$destroy",p);n.removeData("$$animationRunner");
|
||||
C(n,t);ha(n,t);t.domOperation();L&&a.removeClass(n,L);n.removeClass("ng-animate");k.complete(!b)}t=oa(t);var ka=0<=["enter","move","leave"].indexOf(Q),k=new l({end:function(){K()},cancel:function(){K(!0)}});if(!b.length)return K(),k;n.data("$$animationRunner",k);var M=Ea(n.attr("class"),Ea(t.addClass,t.removeClass)),L=t.tempClasses;L&&(M+=" "+L,t.tempClasses=null);var z;ka&&(z="ng-"+Q+"-prepare",a.addClass(n,z));u.push({element:n,classes:M,event:Q,structural:ka,options:t,beforeStart:function(){n.addClass("ng-animate");
|
||||
L&&a.addClass(n,L);z&&(a.removeClass(n,z),z=null)},close:K});n.on("$destroy",p);if(1<u.length)return k;d.$$postDigest(function(){var a=[];s(u,function(b){b.element.data("$$animationRunner")?a.push(b):b.close()});u.length=0;var b=T(a),c=[];s(b,function(a){c.push({domNode:y(a.from?a.from.element:a.element),fn:function(){a.beforeStart();var b,c=a.close;if((a.anchors?a.from.element||a.to.element:a.element).data("$$animationRunner")){var d=x(a);d&&(b=d.start)}b?(b=b(),b.done(function(a){c(!a)}),r(a,b)):
|
||||
c()}})});I(g(c))});return k}}]}]).provider("$animateCss",["$animateProvider",function(a){var b=Ja(),c=Ja();this.$get=["$window","$$jqLite","$$AnimateRunner","$timeout","$$forceReflow","$sniffer","$$rAFScheduler","$$animateQueue",function(a,e,l,n,I,g,u,C){function B(a,b){var c=a.parentNode;return(c.$$ngAnimateParentKey||(c.$$ngAnimateParentKey=++O))+"-"+a.getAttribute("class")+"-"+b}function Q(r,p,g,n){var k;0<b.count(g)&&(k=c.get(g),k||(p=Z(p,"-stagger"),e.addClass(r,p),k=Ha(a,r,n),k.animationDuration=
|
||||
Math.max(k.animationDuration,0),k.transitionDuration=Math.max(k.transitionDuration,0),e.removeClass(r,p),c.put(g,k)));return k||{}}function t(a){x.push(a);u.waitUntilQuiet(function(){b.flush();c.flush();for(var a=I(),d=0;d<x.length;d++)x[d](a);x.length=0})}function H(c,e,g){e=b.get(g);e||(e=Ha(a,c,Ua),"infinite"===e.animationIterationCount&&(e.animationIterationCount=1));b.put(g,e);c=e;g=c.animationDelay;e=c.transitionDelay;c.maxDelay=g&&e?Math.max(g,e):g||e;c.maxDuration=Math.max(c.animationDuration*
|
||||
c.animationIterationCount,c.transitionDuration);return c}var T=V(e),O=0,x=[];return function(a,c){function d(){k()}function u(){k(!0)}function k(b){if(!(w||F&&O)){w=!0;O=!1;f.$$skipPreparationClasses||e.removeClass(a,ga);e.removeClass(a,ea);va(h,!1);pa(h,!1);s(x,function(a){h.style[a[0]]=""});T(a,f);ha(a,f);Object.keys(A).length&&s(A,function(a,b){a?h.style.setProperty(b,a):h.style.removeProperty(b)});if(f.onDone)f.onDone();fa&&fa.length&&a.off(fa.join(" "),z);var c=a.data("$$animateCss");c&&(n.cancel(c[0].timer),
|
||||
a.removeData("$$animateCss"));G&&G.complete(!b)}}function M(a){q.blockTransition&&pa(h,a);q.blockKeyframeAnimation&&va(h,!!a)}function L(){G=new l({end:d,cancel:u});t(P);k();return{$$willAnimate:!1,start:function(){return G},end:d}}function z(a){a.stopPropagation();var b=a.originalEvent||a;a=b.$manualTimeStamp||Date.now();b=parseFloat(b.elapsedTime.toFixed(3));Math.max(a-W,0)>=R&&b>=m&&(F=!0,k())}function N(){function b(){if(!w){M(!1);s(x,function(a){h.style[a[0]]=a[1]});T(a,f);e.addClass(a,ea);if(q.recalculateTimingStyles){na=
|
||||
h.className+" "+ga;ia=B(h,na);D=H(h,na,ia);ca=D.maxDelay;J=Math.max(ca,0);m=D.maxDuration;if(0===m){k();return}q.hasTransitions=0<D.transitionDuration;q.hasAnimations=0<D.animationDuration}q.applyAnimationDelay&&(ca="boolean"!==typeof f.delay&&wa(f.delay)?parseFloat(f.delay):ca,J=Math.max(ca,0),D.animationDelay=ca,da=[qa,ca+"s"],x.push(da),h.style[da[0]]=da[1]);R=1E3*J;V=1E3*m;if(f.easing){var d,g=f.easing;q.hasTransitions&&(d=S+"TimingFunction",x.push([d,g]),h.style[d]=g);q.hasAnimations&&(d=aa+
|
||||
"TimingFunction",x.push([d,g]),h.style[d]=g)}D.transitionDuration&&fa.push(ya);D.animationDuration&&fa.push(za);W=Date.now();var p=R+1.5*V;d=W+p;var g=a.data("$$animateCss")||[],N=!0;if(g.length){var l=g[0];(N=d>l.expectedEndTime)?n.cancel(l.timer):g.push(k)}N&&(p=n(c,p,!1),g[0]={timer:p,expectedEndTime:d},g.push(k),a.data("$$animateCss",g));if(fa.length)a.on(fa.join(" "),z);f.to&&(f.cleanupStyles&&Ka(A,h,Object.keys(f.to)),Ga(a,f))}}function c(){var b=a.data("$$animateCss");if(b){for(var d=1;d<b.length;d++)b[d]();
|
||||
a.removeData("$$animateCss")}}if(!w)if(h.parentNode){var d=function(a){if(F)O&&a&&(O=!1,k());else if(O=!a,D.animationDuration)if(a=va(h,O),O)x.push(a);else{var b=x,c=b.indexOf(a);0<=a&&b.splice(c,1)}},g=0<ba&&(D.transitionDuration&&0===X.transitionDuration||D.animationDuration&&0===X.animationDuration)&&Math.max(X.animationDelay,X.transitionDelay);g?n(b,Math.floor(g*ba*1E3),!1):b();v.resume=function(){d(!0)};v.pause=function(){d(!1)}}else k()}var f=c||{};f.$$prepared||(f=oa(Ba(f)));var A={},h=y(a);
|
||||
if(!h||!h.parentNode||!C.enabled())return L();var x=[],I=a.attr("class"),E=Na(f),w,O,F,G,v,J,R,m,V,W,fa=[];if(0===f.duration||!g.animations&&!g.transitions)return L();var ja=f.event&&Y(f.event)?f.event.join(" "):f.event,$="",U="";ja&&f.structural?$=Z(ja,"ng-",!0):ja&&($=ja);f.addClass&&(U+=Z(f.addClass,"-add"));f.removeClass&&(U.length&&(U+=" "),U+=Z(f.removeClass,"-remove"));f.applyClassesEarly&&U.length&&T(a,f);var ga=[$,U].join(" ").trim(),na=I+" "+ga,ea=Z(ga,"-active"),I=E.to&&0<Object.keys(E.to).length;
|
||||
if(!(0<(f.keyframeStyle||"").length||I||ga))return L();var ia,X;0<f.stagger?(E=parseFloat(f.stagger),X={transitionDelay:E,animationDelay:E,transitionDuration:0,animationDuration:0}):(ia=B(h,na),X=Q(h,ga,ia,Va));f.$$skipPreparationClasses||e.addClass(a,ga);f.transitionStyle&&(E=[S,f.transitionStyle],la(h,E),x.push(E));0<=f.duration&&(E=0<h.style[S].length,E=Ia(f.duration,E),la(h,E),x.push(E));f.keyframeStyle&&(E=[aa,f.keyframeStyle],la(h,E),x.push(E));var ba=X?0<=f.staggerIndex?f.staggerIndex:b.count(ia):
|
||||
0;(ja=0===ba)&&!f.skipBlocking&&pa(h,9999);var D=H(h,na,ia),ca=D.maxDelay;J=Math.max(ca,0);m=D.maxDuration;var q={};q.hasTransitions=0<D.transitionDuration;q.hasAnimations=0<D.animationDuration;q.hasTransitionAll=q.hasTransitions&&"all"==D.transitionProperty;q.applyTransitionDuration=I&&(q.hasTransitions&&!q.hasTransitionAll||q.hasAnimations&&!q.hasTransitions);q.applyAnimationDuration=f.duration&&q.hasAnimations;q.applyTransitionDelay=wa(f.delay)&&(q.applyTransitionDuration||q.hasTransitions);q.applyAnimationDelay=
|
||||
wa(f.delay)&&q.hasAnimations;q.recalculateTimingStyles=0<U.length;if(q.applyTransitionDuration||q.applyAnimationDuration)m=f.duration?parseFloat(f.duration):m,q.applyTransitionDuration&&(q.hasTransitions=!0,D.transitionDuration=m,E=0<h.style[S+"Property"].length,x.push(Ia(m,E))),q.applyAnimationDuration&&(q.hasAnimations=!0,D.animationDuration=m,x.push([Aa,m+"s"]));if(0===m&&!q.recalculateTimingStyles)return L();if(null!=f.delay){var da;"boolean"!==typeof f.delay&&(da=parseFloat(f.delay),J=Math.max(da,
|
||||
0));q.applyTransitionDelay&&x.push([ma,da+"s"]);q.applyAnimationDelay&&x.push([qa,da+"s"])}null==f.duration&&0<D.transitionDuration&&(q.recalculateTimingStyles=q.recalculateTimingStyles||ja);R=1E3*J;V=1E3*m;f.skipBlocking||(q.blockTransition=0<D.transitionDuration,q.blockKeyframeAnimation=0<D.animationDuration&&0<X.animationDelay&&0===X.animationDuration);f.from&&(f.cleanupStyles&&Ka(A,h,Object.keys(f.from)),Fa(a,f));q.blockTransition||q.blockKeyframeAnimation?M(m):f.skipBlocking||pa(h,!1);return{$$willAnimate:!0,
|
||||
end:d,start:function(){if(!w)return v={end:d,cancel:u,resume:null,pause:null},G=new l(v),t(N),G}}}}]}]).provider("$$animateCssDriver",["$$animationProvider",function(a){a.drivers.push("$$animateCssDriver");this.$get=["$animateCss","$rootScope","$$AnimateRunner","$rootElement","$sniffer","$$jqLite","$document",function(a,c,d,e,l,n,I){function g(a){return a.replace(/\bng-\S+\b/g,"")}function u(a,b){G(a)&&(a=a.split(" "));G(b)&&(b=b.split(" "));return a.filter(function(a){return-1===b.indexOf(a)}).join(" ")}
|
||||
function C(c,e,n){function l(a){var b={},c=y(a).getBoundingClientRect();s(["width","height","top","left"],function(a){var d=c[a];switch(a){case "top":d+=t.scrollTop;break;case "left":d+=t.scrollLeft}b[a]=Math.floor(d)+"px"});return b}function p(){var c=g(n.attr("class")||""),d=u(c,k),c=u(k,c),d=a(C,{to:l(n),addClass:"ng-anchor-in "+d,removeClass:"ng-anchor-out "+c,delay:!0});return d.$$willAnimate?d:null}function I(){C.remove();e.removeClass("ng-animate-shim");n.removeClass("ng-animate-shim")}var C=
|
||||
F(y(e).cloneNode(!0)),k=g(C.attr("class")||"");e.addClass("ng-animate-shim");n.addClass("ng-animate-shim");C.addClass("ng-anchor");H.append(C);var M;c=function(){var c=a(C,{addClass:"ng-anchor-out",delay:!0,from:l(e)});return c.$$willAnimate?c:null}();if(!c&&(M=p(),!M))return I();var L=c||M;return{start:function(){function a(){c&&c.end()}var b,c=L.start();c.done(function(){c=null;if(!M&&(M=p()))return c=M.start(),c.done(function(){c=null;I();b.complete()}),c;I();b.complete()});return b=new d({end:a,
|
||||
cancel:a})}}}function B(a,b,c,e){var g=Q(a,P),n=Q(b,P),l=[];s(e,function(a){(a=C(c,a.out,a["in"]))&&l.push(a)});if(g||n||0!==l.length)return{start:function(){function a(){s(b,function(a){a.end()})}var b=[];g&&b.push(g.start());n&&b.push(n.start());s(l,function(a){b.push(a.start())});var c=new d({end:a,cancel:a});d.all(b,function(a){c.complete(a)});return c}}}function Q(c){var d=c.element,e=c.options||{};c.structural&&(e.event=c.event,e.structural=!0,e.applyClassesEarly=!0,"leave"===c.event&&(e.onDone=
|
||||
e.domOperation));e.preparationClasses&&(e.event=$(e.event,e.preparationClasses));c=a(d,e);return c.$$willAnimate?c:null}if(!l.animations&&!l.transitions)return P;var t=I[0].body;c=y(e);var H=F(c.parentNode&&11===c.parentNode.nodeType||t.contains(c)?c:t);V(n);return function(a){return a.from&&a.to?B(a.from,a.to,a.classes,a.anchors):Q(a)}}]}]).provider("$$animateJs",["$animateProvider",function(a){this.$get=["$injector","$$AnimateRunner","$$jqLite",function(b,c,d){function e(c){c=Y(c)?c:c.split(" ");
|
||||
for(var d=[],e={},l=0;l<c.length;l++){var s=c[l],B=a.$$registeredAnimations[s];B&&!e[s]&&(d.push(b.get(B)),e[s]=!0)}return d}var l=V(d);return function(a,b,d,u){function C(){u.domOperation();l(a,u)}function B(a,b,d,e,f){switch(d){case "animate":b=[b,e.from,e.to,f];break;case "setClass":b=[b,F,G,f];break;case "addClass":b=[b,F,f];break;case "removeClass":b=[b,G,f];break;default:b=[b,f]}b.push(e);if(a=a.apply(a,b))if(Ca(a.start)&&(a=a.start()),a instanceof c)a.done(f);else if(Ca(a))return a;return P}
|
||||
function y(a,b,d,e,f){var g=[];s(e,function(e){var k=e[f];k&&g.push(function(){var e,f,g=!1,h=function(a){g||(g=!0,(f||P)(a),e.complete(!a))};e=new c({end:function(){h()},cancel:function(){h(!0)}});f=B(k,a,b,d,function(a){h(!1===a)});return e})});return g}function t(a,b,d,e,f){var g=y(a,b,d,e,f);if(0===g.length){var h,k;"beforeSetClass"===f?(h=y(a,"removeClass",d,e,"beforeRemoveClass"),k=y(a,"addClass",d,e,"beforeAddClass")):"setClass"===f&&(h=y(a,"removeClass",d,e,"removeClass"),k=y(a,"addClass",
|
||||
d,e,"addClass"));h&&(g=g.concat(h));k&&(g=g.concat(k))}if(0!==g.length)return function(a){var b=[];g.length&&s(g,function(a){b.push(a())});b.length?c.all(b,a):a();return function(a){s(b,function(b){a?b.cancel():b.end()})}}}var H=!1;3===arguments.length&&ba(d)&&(u=d,d=null);u=oa(u);d||(d=a.attr("class")||"",u.addClass&&(d+=" "+u.addClass),u.removeClass&&(d+=" "+u.removeClass));var F=u.addClass,G=u.removeClass,x=e(d),r,p;if(x.length){var K,J;"leave"==b?(J="leave",K="afterLeave"):(J="before"+b.charAt(0).toUpperCase()+
|
||||
b.substr(1),K=b);"enter"!==b&&"move"!==b&&(r=t(a,b,u,x,J));p=t(a,b,u,x,K)}if(r||p){var k;return{$$willAnimate:!0,end:function(){k?k.end():(H=!0,C(),ha(a,u),k=new c,k.complete(!0));return k},start:function(){function b(c){H=!0;C();ha(a,u);k.complete(c)}if(k)return k;k=new c;var d,e=[];r&&e.push(function(a){d=r(a)});e.length?e.push(function(a){C();a(!0)}):C();p&&e.push(function(a){d=p(a)});k.setHost({end:function(){H||((d||P)(void 0),b(void 0))},cancel:function(){H||((d||P)(!0),b(!0))}});c.chain(e,
|
||||
b);return k}}}}}]}]).provider("$$animateJsDriver",["$$animationProvider",function(a){a.drivers.push("$$animateJsDriver");this.$get=["$$animateJs","$$AnimateRunner",function(a,c){function d(c){return a(c.element,c.event,c.classes,c.options)}return function(a){if(a.from&&a.to){var b=d(a.from),n=d(a.to);if(b||n)return{start:function(){function a(){return function(){s(d,function(a){a.end()})}}var d=[];b&&d.push(b.start());n&&d.push(n.start());c.all(d,function(a){e.complete(a)});var e=new c({end:a(),cancel:a()});
|
||||
return e}}}else return d(a)}}]}])})(window,window.angular);
|
||||
//# sourceMappingURL=angular-animate.min.js.map
|
||||
@@ -1 +0,0 @@
|
||||
!function(){"use strict";angular.module("base64",[]).constant("$base64",function(){function a(a,b){var c=f.indexOf(a.charAt(b));if(-1==c)throw"Cannot decode base64";return c}function b(b){b=""+b;var c,d,f,g=b.length;if(0==g)return b;if(0!=g%4)throw"Cannot decode base64";c=0,b.charAt(g-1)==e&&(c=1,b.charAt(g-2)==e&&(c=2),g-=4);var h=[];for(d=0;g>d;d+=4)f=a(b,d)<<18|a(b,d+1)<<12|a(b,d+2)<<6|a(b,d+3),h.push(String.fromCharCode(f>>16,255&f>>8,255&f));switch(c){case 1:f=a(b,d)<<18|a(b,d+1)<<12|a(b,d+2)<<6,h.push(String.fromCharCode(f>>16,255&f>>8));break;case 2:f=a(b,d)<<18|a(b,d+1)<<12,h.push(String.fromCharCode(f>>16))}return h.join("")}function c(a,b){var c=a.charCodeAt(b);if(c>255)throw"INVALID_CHARACTER_ERR: DOM Exception 5";return c}function d(a){if(1!=arguments.length)throw"SyntaxError: Not enough arguments";var b,d,g=[];a=""+a;var h=a.length-a.length%3;if(0==a.length)return a;for(b=0;h>b;b+=3)d=c(a,b)<<16|c(a,b+1)<<8|c(a,b+2),g.push(f.charAt(d>>18)),g.push(f.charAt(63&d>>12)),g.push(f.charAt(63&d>>6)),g.push(f.charAt(63&d));switch(a.length-h){case 1:d=c(a,b)<<16,g.push(f.charAt(d>>18)+f.charAt(63&d>>12)+e+e);break;case 2:d=c(a,b)<<16|c(a,b+1)<<8,g.push(f.charAt(d>>18)+f.charAt(63&d>>12)+f.charAt(63&d>>6)+e)}return g.join("")}var e="=",f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";return{encode:d,decode:b}}())}();
|
||||
@@ -1,361 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// -------------------------------
|
||||
// WARNING
|
||||
// -------------------------------
|
||||
// This file is taken from https://github.com/sebastianha/angular-bootstrap-multiselect
|
||||
// There are local modifications like support for translation
|
||||
// -------------------------------
|
||||
|
||||
angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
//from bootstrap-ui typeahead parser
|
||||
.factory("optionParser", ["$parse", function($parse) {
|
||||
// 00000111000000000000022200000000000000003333333333333330000000000044000
|
||||
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
|
||||
|
||||
return {
|
||||
parse: function(input) {
|
||||
|
||||
var match = input.match(TYPEAHEAD_REGEXP);
|
||||
if(!match) {
|
||||
throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + " but got '" + input + "'.");
|
||||
}
|
||||
|
||||
return {
|
||||
itemName : match[3],
|
||||
source : $parse(match[4]),
|
||||
viewMapper : $parse(match[2] || match[1]),
|
||||
modelMapper: $parse(match[1])
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "$translate", "optionParser", function($parse, $document, $compile, $interpolate, $translate, optionParser) {
|
||||
return {
|
||||
restrict: "E",
|
||||
require : "ngModel",
|
||||
link : function(originalScope, element, attrs, modelCtrl) {
|
||||
|
||||
var exp = attrs.options;
|
||||
var parsedResult = optionParser.parse(exp);
|
||||
var isMultiple = attrs.multiple ? true : false;
|
||||
var compareByKey = attrs.compareBy;
|
||||
var headerKey = attrs.headerKey;
|
||||
var dividerKey = attrs.dividerKey;
|
||||
var scrollAfterRows = attrs.scrollAfterRows;
|
||||
var tabindex = attrs.tabindex;
|
||||
var maxWidth = attrs.maxWidth;
|
||||
var required = false;
|
||||
var scope = originalScope.$new();
|
||||
scope.filterAfterRows = attrs.filterAfterRows;
|
||||
var changeHandler = attrs.change || angular.noop;
|
||||
|
||||
scope.items = [];
|
||||
scope.header = "Select";
|
||||
scope.multiple = isMultiple;
|
||||
scope.disabled = false;
|
||||
|
||||
scope.ulStyle = {};
|
||||
if(scrollAfterRows !== undefined && parseInt(scrollAfterRows).toString() === scrollAfterRows) {
|
||||
scope.ulStyle = {"max-height": (scrollAfterRows*26+14)+"px", "overflow-y": "auto", "overflow-x": "hidden"};
|
||||
}
|
||||
if(tabindex !== undefined && parseInt(tabindex).toString() === tabindex) {
|
||||
scope.tabindex = tabindex;
|
||||
}
|
||||
if(maxWidth !== undefined && parseInt(maxWidth).toString() === maxWidth) {
|
||||
scope.maxWidth = {"max-width": maxWidth + "px"};
|
||||
}
|
||||
|
||||
originalScope.$on("$destroy", function() {
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
var popUpEl = angular.element("<multiselect-popup></multiselect-popup>");
|
||||
|
||||
//required validator
|
||||
if(attrs.required || attrs.ngRequired) {
|
||||
required = true;
|
||||
}
|
||||
attrs.$observe("required", function(newVal) {
|
||||
required = newVal;
|
||||
});
|
||||
|
||||
//watch disabled state
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.ngDisabled)(originalScope);
|
||||
}, function(newVal) {
|
||||
scope.disabled = newVal;
|
||||
});
|
||||
|
||||
//watch single/multiple state for dynamically change single to multiple
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.multiple)(originalScope);
|
||||
}, function(newVal) {
|
||||
isMultiple = newVal || false;
|
||||
});
|
||||
|
||||
//watch option changes for options that are populated dynamically
|
||||
scope.$watch(function() {
|
||||
return parsedResult.source(originalScope);
|
||||
}, function(newVal) {
|
||||
if(angular.isDefined(newVal)) {
|
||||
parseModel();
|
||||
}
|
||||
}, true);
|
||||
|
||||
//watch model change
|
||||
scope.$watch(function() {
|
||||
return modelCtrl.$modelValue;
|
||||
}, function(newVal, oldVal) {
|
||||
//when directive initialize, newVal usually undefined. Also, if model value already set in the controller
|
||||
//for preselected list then we need to mark checked in our scope item. But we don't want to do this every time
|
||||
//model changes. We need to do this only if it is done outside directive scope, from controller, for example.
|
||||
if(angular.isDefined(newVal)) {
|
||||
markChecked(newVal);
|
||||
scope.$eval(changeHandler);
|
||||
}
|
||||
getHeaderText();
|
||||
modelCtrl.$setValidity("required", scope.valid());
|
||||
});
|
||||
|
||||
function parseModel() {
|
||||
scope.items.length = 0;
|
||||
var model = parsedResult.source(originalScope);
|
||||
if(!angular.isDefined(model) || model === null) {
|
||||
return;
|
||||
}
|
||||
for(var i = 0; i < model.length; i++) {
|
||||
var local = {};
|
||||
local[parsedResult.itemName] = model[i];
|
||||
|
||||
// calculate checked status of the option
|
||||
// https://github.com/sebastianha/angular-bootstrap-multiselect/pull/4/files
|
||||
var id = model[i];
|
||||
var checked = false;
|
||||
var modelValue = modelCtrl.$modelValue;
|
||||
if (modelValue) {
|
||||
if (angular.isArray(modelValue)) {
|
||||
for (var j = 0; j < modelValue.length; j++)
|
||||
if (modelValue[j] == id) {
|
||||
checked = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
checked = modelValue == id;
|
||||
}
|
||||
}
|
||||
|
||||
scope.items.push({
|
||||
label : parsedResult.viewMapper(local),
|
||||
model : model[i],
|
||||
checked: checked,
|
||||
header : model[i][headerKey],
|
||||
divider : model[i][dividerKey]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseModel();
|
||||
|
||||
element.append($compile(popUpEl)(scope));
|
||||
|
||||
function getHeaderText() {
|
||||
if(isEmpty(modelCtrl.$modelValue)) {
|
||||
scope.header = attrs.msHeader || $translate.instant('main.multiselect.select');
|
||||
return scope.header;
|
||||
}
|
||||
|
||||
if(isMultiple) {
|
||||
if(attrs.msSelected) {
|
||||
scope.header = $interpolate(attrs.msSelected)(scope);
|
||||
} else {
|
||||
scope.header = $translate.instant('main.multiselect.selected', { n: modelCtrl.$modelValue.length });
|
||||
}
|
||||
} else {
|
||||
var local = {};
|
||||
local[parsedResult.itemName] = modelCtrl.$modelValue;
|
||||
scope.header = parsedResult.viewMapper(local);
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(obj) {
|
||||
if(obj === true || obj === false) {
|
||||
return false;
|
||||
}
|
||||
if(!obj) {
|
||||
return true;
|
||||
}
|
||||
if(obj.length && obj.length > 0) {
|
||||
return false;
|
||||
}
|
||||
for(var prop in obj) {
|
||||
if(obj[prop]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(compareByKey !== undefined && obj[compareByKey] !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
scope.valid = function validModel() {
|
||||
if(!required) {
|
||||
return true;
|
||||
}
|
||||
var value = modelCtrl.$modelValue;
|
||||
return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value !== null);
|
||||
};
|
||||
|
||||
function selectSingle(item) {
|
||||
if(!item.checked) {
|
||||
scope.uncheckAll();
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
setModelValue(false);
|
||||
}
|
||||
|
||||
function selectMultiple(item) {
|
||||
item.checked = !item.checked;
|
||||
setModelValue(true);
|
||||
}
|
||||
|
||||
function setModelValue(isMultiple) {
|
||||
var value;
|
||||
|
||||
if(isMultiple) {
|
||||
value = [];
|
||||
angular.forEach(scope.items, function(item) {
|
||||
if(item.checked) {
|
||||
value.push(item.model);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
if(item.checked) {
|
||||
value = item.model;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
modelCtrl.$setViewValue(value);
|
||||
}
|
||||
|
||||
function markChecked(newVal) {
|
||||
if(!angular.isArray(newVal)) {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
if(compareByKey === undefined && angular.equals(item.model, newVal)) {
|
||||
item.checked = true;
|
||||
} else if(compareByKey !== undefined && newVal !== null && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], newVal[compareByKey])) {
|
||||
item.checked = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
angular.forEach(newVal, function(i) {
|
||||
if(compareByKey === undefined && angular.equals(item.model, i)) {
|
||||
item.checked = true;
|
||||
} else if(compareByKey !== undefined && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], i[compareByKey])) {
|
||||
item.checked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scope.checkAll = function() {
|
||||
if(!isMultiple) {
|
||||
return;
|
||||
}
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = true;
|
||||
});
|
||||
setModelValue(true);
|
||||
};
|
||||
|
||||
scope.uncheckAll = function() {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
});
|
||||
setModelValue(true);
|
||||
};
|
||||
|
||||
scope.select = function(event, item) {
|
||||
if(isMultiple === false) {
|
||||
selectSingle(item);
|
||||
scope.toggleSelect();
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
selectMultiple(item);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselectPopup", ["$document", function($document) {
|
||||
return {
|
||||
restrict : "E",
|
||||
scope : false,
|
||||
replace : true,
|
||||
templateUrl: "multiselect.tpl.html",
|
||||
link : function(scope, element, attrs) {
|
||||
|
||||
scope.isVisible = false;
|
||||
|
||||
scope.toggleSelect = function() {
|
||||
if(element.hasClass("open")) {
|
||||
scope.filter = "";
|
||||
element.removeClass("open");
|
||||
$document.unbind("click", clickHandler);
|
||||
} else {
|
||||
scope.filter = "";
|
||||
element.addClass("open");
|
||||
$document.bind("click", clickHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// $("ul.dropdown-menu").on("click", "[data-stopPropagation]", function(e) {
|
||||
// e.stopPropagation();
|
||||
// });
|
||||
|
||||
function clickHandler(event) {
|
||||
if(elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) {
|
||||
return;
|
||||
}
|
||||
element.removeClass("open");
|
||||
$document.unbind("click", clickHandler);
|
||||
scope.$apply();
|
||||
}
|
||||
|
||||
var elementMatchesAnyInArray = function(element, elementArray) {
|
||||
for(var i = 0; i < elementArray.length; i++) {
|
||||
if(element === elementArray[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
angular.module("multiselect.tpl.html", []).run(["$templateCache", function($templateCache) {
|
||||
$templateCache.put("multiselect.tpl.html",
|
||||
"<div class=\"btn-group\">\n" +
|
||||
" <button tabindex=\"{{tabindex}}\" title=\"{{header}}\" type=\"button\" class=\"btn btn-default dropdown-toggle\" ng-click=\"toggleSelect()\" ng-disabled=\"disabled\" ng-class=\"{'error': !valid()}\">\n" +
|
||||
" <div ng-style=\"maxWidth\" style=\"padding-right: 13px; overflow: hidden; text-overflow: ellipsis;\">{{header}}</div><span class=\"caret\" style=\"position:absolute;right:10px;top:14px;\"></span>\n" +
|
||||
" </button>\n" +
|
||||
" <ul class=\"dropdown-menu\" style=\"margin-bottom:30px;padding-left:5px;padding-right:5px;\" ng-style=\"ulStyle\">\n" +
|
||||
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px; margin-bottom: 4px;\" placeholder=\"{{ 'main.multiselect.filterPlaceholder' | tr }}\">" +
|
||||
" <li data-stopPropagation=\"true\" ng-repeat=\"i in items | filter:filter\" ng-class=\"{'dropdown-header': i.header, 'divider': i.divider}\">\n" +
|
||||
" <a ng-if=\"!i.header && !i.divider\" ng-click=\"select($event, i)\" style=\"padding:3px 10px;cursor:pointer;\">\n" +
|
||||
" <i class=\"fa\" ng-class=\"{'fa-check': i.checked, 'empty': !i.checked}\"></i> {{i.label}}" +
|
||||
" </a>\n" +
|
||||
" <span ng-if=\"i.header\">{{i.label}}</span>" +
|
||||
" </li>\n" +
|
||||
" </ul>\n" +
|
||||
"</div>");
|
||||
}]);
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(n,c){'use strict';function l(b,a,g){var d=g.baseHref(),k=b[0];return function(b,e,f){var g,h;f=f||{};h=f.expires;g=c.isDefined(f.path)?f.path:d;c.isUndefined(e)&&(h="Thu, 01 Jan 1970 00:00:00 GMT",e="");c.isString(h)&&(h=new Date(h));e=encodeURIComponent(b)+"="+encodeURIComponent(e);e=e+(g?";path="+g:"")+(f.domain?";domain="+f.domain:"");e+=h?";expires="+h.toUTCString():"";e+=f.secure?";secure":"";f=e.length+1;4096<f&&a.warn("Cookie '"+b+"' possibly not set or overflowed because it was too large ("+
|
||||
f+" > 4096 bytes)!");k.cookie=e}}c.module("ngCookies",["ng"]).provider("$cookies",[function(){var b=this.defaults={};this.$get=["$$cookieReader","$$cookieWriter",function(a,g){return{get:function(d){return a()[d]},getObject:function(d){return(d=this.get(d))?c.fromJson(d):d},getAll:function(){return a()},put:function(d,a,m){g(d,a,m?c.extend({},b,m):b)},putObject:function(d,b,a){this.put(d,c.toJson(b),a)},remove:function(a,k){g(a,void 0,k?c.extend({},b,k):b)}}}]}]);c.module("ngCookies").factory("$cookieStore",
|
||||
["$cookies",function(b){return{get:function(a){return b.getObject(a)},put:function(a,c){b.putObject(a,c)},remove:function(a){b.remove(a)}}}]);l.$inject=["$document","$log","$browser"];c.module("ngCookies").provider("$$cookieWriter",function(){this.$get=l})})(window,window.angular);
|
||||
//# sourceMappingURL=angular-cookies.min.js.map
|
||||
@@ -1 +0,0 @@
|
||||
!function(t,e,i,n){"use strict";i.module("ngFitText",[]).value("fitTextDefaultConfig",{debounce:!1,delay:250,loadDelay:10,compressor:1,min:0,max:Number.POSITIVE_INFINITY}).directive("fittext",["$timeout","fitTextDefaultConfig","fitTextConfig",function(e,n,o){return{restrict:"A",scope:!0,link:function(f,a,l){function r(){var t=T*h/s.offsetWidth/h;return Math.max(Math.min((c[0].offsetWidth-6)*t*p,parseFloat(y)),parseFloat(m))}function u(){s.offsetHeight*s.offsetWidth!==0&&(d.fontSize=T+"px",d.lineHeight="1",d.display="inline-block",d.fontSize=r()+"px",d.lineHeight=b,d.display=v)}i.extend(n,o.config);var c=a.parent(),s=a[0],d=s.style,x=t.getComputedStyle(a[0],null),h=a.children().length||1,g=l.fittextLoadDelay||n.loadDelay,p=l.fittext||n.compressor,m=("inherit"===l.fittextMin?x["font-size"]:l.fittextMin)||n.min,y=("inherit"===l.fittextMax?x["font-size"]:l.fittextMax)||n.max,b=x["line-height"],v=x.display,T=10;e(function(){u()},g),f.$watch(l.ngBind,function(){u()}),n.debounce?i.element(t).bind("resize",n.debounce(function(){f.$apply(u)},n.delay)):i.element(t).bind("resize",function(){f.$apply(u)})}}}]).provider("fitTextConfig",function(){var t=this;return this.config={},this.$get=function(){var e={};return e.config=t.config,e},this})}(window,document,angular);
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(){'use strict';function d(b){return function(){var a=arguments[0],e;e="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.5.8/"+(b?b+"/":"")+a;for(a=1;a<arguments.length;a++){e=e+(1==a?"?":"&")+"p"+(a-1)+"=";var d=encodeURIComponent,c;c=arguments[a];c="function"==typeof c?c.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof c?"undefined":"string"!=typeof c?JSON.stringify(c):c;e+=d(c)}return Error(e)}}(function(b){function a(c,a,b){return c[a]||(c[a]=b())}var e=d("$injector"),n=d("ng");
|
||||
b=a(b,"angular",Object);b.$$minErr=b.$$minErr||d;return a(b,"module",function(){var c={};return function(b,d,h){if("hasOwnProperty"===b)throw n("badname","module");d&&c.hasOwnProperty(b)&&(c[b]=null);return a(c,b,function(){function c(a,b,d,e){e||(e=f);return function(){e[d||"push"]([a,b,arguments]);return g}}function a(c,e){return function(a,d){d&&"function"===typeof d&&(d.$$moduleName=b);f.push([c,e,arguments]);return g}}if(!d)throw e("nomod",b);var f=[],k=[],l=[],m=c("$injector","invoke","push",
|
||||
k),g={_invokeQueue:f,_configBlocks:k,_runBlocks:l,requires:d,name:b,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:c("$provide","value"),constant:c("$provide","constant","unshift"),decorator:a("$provide","decorator"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),component:a("$compileProvider","component"),config:m,run:function(a){l.push(a);
|
||||
return this}};h&&m(h);return g})}})})(window)})(window);
|
||||
//# sourceMappingURL=angular-loader.min.js.map
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"version":3,
|
||||
"file":"angular-loader.min.js",
|
||||
"lineCount":9,
|
||||
"mappings":"A;;;;;aAMC,SAAQ,EAAG,CA6DZA,QAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,MAAAA,SAAAA,EAAAA,CAAAA,IAAAA,EAAAA,SAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,EAAAA,CAAAA,GAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,GAAAA,CAAAA,EAAAA,EAAAA,CAAAA,CAAAA,sCAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,GAAAA,CAAAA,EAAAA,EAAAA,CAAAA,KAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,SAAAA,OAAAA,CAAAA,CAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,EAAAA,CAAAA,EAAAA,CAAAA,CAAAA,GAAAA,CAAAA,GAAAA,EAAAA,GAAAA,EAAAA,CAAAA,CAAAA,CAAAA,EAAAA,GAAAA,KAAAA,EAAAA,kBAAAA,CAAAA,CAAAA,EAAAA,CAAAA,SAAAA,CAAAA,CAAAA,CAAAA,EAAAA,CAAAA,UAAAA,EAAAA,MAAAA,EAAAA,CAAAA,CAAAA,SAAAA,EAAAA,QAAAA,CAAAA,aAAAA,CAAAA,EAAAA,CAAAA,CAAAA,WAAAA,EAAAA,MAAAA,EAAAA,CAAAA,WAAAA,CAAAA,QAAAA,EAAAA,MAAAA,EAAAA,CAAAA,IAAAA,UAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,EAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,MAAAA,MAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CA2CAC,SAA0B,CAACC,CAAD,CAAS,CAKjCC,QAASA,EAAM,CAACC,CAAD,CAAMC,CAAN,CAAYC,CAAZ,CAAqB,CAClC,MAAOF,EAAA,CAAIC,CAAJ,CAAP,GAAqBD,CAAA,CAAIC,CAAJ,CAArB,CAAiCC,CAAA,EAAjC,CADkC,CAHpC,IAAIC,EAAkBP,CAAA,CAAO,WAAP,CAAtB,CACIQ,EAAWR,CAAA,CAAO,IAAP,CAMXS;CAAAA,CAAUN,CAAA,CAAOD,CAAP,CAAe,SAAf,CAA0BQ,MAA1B,CAGdD,EAAAE,SAAA,CAAmBF,CAAAE,SAAnB,EAAuCX,CAEvC,OAAOG,EAAA,CAAOM,CAAP,CAAgB,QAAhB,CAA0B,QAAQ,EAAG,CAE1C,IAAIG,EAAU,EAqDd,OAAOC,SAAe,CAACR,CAAD,CAAOS,CAAP,CAAiBC,CAAjB,CAA2B,CAE7C,GAAa,gBAAb,GAKsBV,CALtB,CACE,KAAMG,EAAA,CAAS,SAAT,CAIoBQ,QAJpB,CAAN,CAKAF,CAAJ,EAAgBF,CAAAK,eAAA,CAAuBZ,CAAvB,CAAhB,GACEO,CAAA,CAAQP,CAAR,CADF,CACkB,IADlB,CAGA,OAAOF,EAAA,CAAOS,CAAP,CAAgBP,CAAhB,CAAsB,QAAQ,EAAG,CAuPtCa,QAASA,EAAW,CAACC,CAAD,CAAWC,CAAX,CAAmBC,CAAnB,CAAiCC,CAAjC,CAAwC,CACrDA,CAAL,GAAYA,CAAZ,CAAoBC,CAApB,CACA,OAAO,SAAQ,EAAG,CAChBD,CAAA,CAAMD,CAAN,EAAsB,MAAtB,CAAA,CAA8B,CAACF,CAAD,CAAWC,CAAX,CAAmBI,SAAnB,CAA9B,CACA,OAAOC,EAFS,CAFwC,CAa5DC,QAASA,EAA2B,CAACP,CAAD,CAAWC,CAAX,CAAmB,CACrD,MAAO,SAAQ,CAACO,CAAD,CAAaC,CAAb,CAA8B,CACvCA,CAAJ,EA7b4C,UA6b5C,GA7b2B,MA6bOA,EAAlC,GAAoDA,CAAAC,aAApD,CAAmFxB,CAAnF,CACAkB,EAAAO,KAAA,CAAiB,CAACX,CAAD,CAAWC,CAAX,CAAmBI,SAAnB,CAAjB,CACA,OAAOC,EAHoC,CADQ,CAnQvD,GAAKX,CAAAA,CAAL,CACE,KAAMP,EAAA,CAAgB,OAAhB,CAEiDF,CAFjD,CAAN,CAMF,IAAIkB,EAAc,EAAlB,CAGIQ,EAAe,EAHnB,CAMIC,EAAY,EANhB,CAQIC,EAASf,CAAA,CAAY,WAAZ,CAAyB,QAAzB,CAAmC,MAAnC;AAA2Ca,CAA3C,CARb,CAWIN,EAAiB,CAEnBS,aAAcX,CAFK,CAGnBY,cAAeJ,CAHI,CAInBK,WAAYJ,CAJO,CAenBlB,SAAUA,CAfS,CAyBnBT,KAAMA,CAzBa,CAsCnBc,SAAUO,CAAA,CAA4B,UAA5B,CAAwC,UAAxC,CAtCS,CAiDnBpB,QAASoB,CAAA,CAA4B,UAA5B,CAAwC,SAAxC,CAjDU,CA4DnBW,QAASX,CAAA,CAA4B,UAA5B,CAAwC,SAAxC,CA5DU,CAuEnBY,MAAOpB,CAAA,CAAY,UAAZ,CAAwB,OAAxB,CAvEY,CAmFnBqB,SAAUrB,CAAA,CAAY,UAAZ,CAAwB,UAAxB,CAAoC,SAApC,CAnFS,CA+FnBsB,UAAWd,CAAA,CAA4B,UAA5B,CAAwC,WAAxC,CA/FQ,CAiInBe,UAAWf,CAAA,CAA4B,kBAA5B,CAAgD,UAAhD,CAjIQ,CAmJnBgB,OAAQhB,CAAA,CAA4B,iBAA5B,CAA+C,UAA/C,CAnJW,CA+JnBiB,WAAYjB,CAAA,CAA4B,qBAA5B,CAAmD,UAAnD,CA/JO,CA4KnBkB,UAAWlB,CAAA,CAA4B,kBAA5B,CAAgD,WAAhD,CA5KQ,CAyLnBmB,UAAWnB,CAAA,CAA4B,kBAA5B,CAAgD,WAAhD,CAzLQ,CAsMnBO,OAAQA,CAtMW,CAkNnBa,IAAKA,QAAQ,CAACC,CAAD,CAAQ,CACnBf,CAAAF,KAAA,CAAeiB,CAAf,CACA;MAAO,KAFY,CAlNF,CAwNjBhC,EAAJ,EACEkB,CAAA,CAAOlB,CAAP,CAGF,OAAOU,EA/O+B,CAAjC,CAXwC,CAvDP,CAArC,CAd0B,CAAnCxB,CAiWA,CAAkBC,MAAlB,CAzcY,CAAX,CAAD,CA0cGA,MA1cH;",
|
||||
"sources":["angular-loader.js"],
|
||||
"names":["minErr","setupModuleLoader","window","ensure","obj","name","factory","$injectorMinErr","ngMinErr","angular","Object","$$minErr","modules","module","requires","configFn","context","hasOwnProperty","invokeLater","provider","method","insertMethod","queue","invokeQueue","arguments","moduleInstance","invokeLaterAndSetModuleName","recipeName","factoryFunction","$$moduleName","push","configBlocks","runBlocks","config","_invokeQueue","_configBlocks","_runBlocks","service","value","constant","decorator","animation","filter","controller","directive","component","run","block"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/*
|
||||
angular-md5 - v0.1.7
|
||||
2014-01-20
|
||||
*/
|
||||
|
||||
!function(a,b){b.module("angular-md5",["gdi2290.md5"]),b.module("ngMd5",["gdi2290.md5"]),b.module("gdi2290.md5",["gdi2290.gravatar-filter","gdi2290.md5-service","gdi2290.md5-filter"]),b.module("gdi2290.gravatar-filter",[]).filter("gravatar",["md5",function(a){var b={};return function(c,d){return b[c]||(d=d?a.createHash(d.toString().toLowerCase()):"",b[c]=c?a.createHash(c.toString().toLowerCase()):d),b[c]}}]),b.module("gdi2290.md5-filter",[]).filter("md5",["md5",function(a){return function(b){return b?a.createHash(b.toString().toLowerCase()):b}}]),b.module("gdi2290.md5-service",[]).factory("md5",[function(){var a={createHash:function(a){var b,c,d,e,f,g,h,i,j,k,l=function(a,b){return a<<b|a>>>32-b},m=function(a,b){var c,d,e,f,g;return e=2147483648&a,f=2147483648&b,c=1073741824&a,d=1073741824&b,g=(1073741823&a)+(1073741823&b),c&d?2147483648^g^e^f:c|d?1073741824&g?3221225472^g^e^f:1073741824^g^e^f:g^e^f},n=function(a,b,c){return a&b|~a&c},o=function(a,b,c){return a&c|b&~c},p=function(a,b,c){return a^b^c},q=function(a,b,c){return b^(a|~c)},r=function(a,b,c,d,e,f,g){return a=m(a,m(m(n(b,c,d),e),g)),m(l(a,f),b)},s=function(a,b,c,d,e,f,g){return a=m(a,m(m(o(b,c,d),e),g)),m(l(a,f),b)},t=function(a,b,c,d,e,f,g){return a=m(a,m(m(p(b,c,d),e),g)),m(l(a,f),b)},u=function(a,b,c,d,e,f,g){return a=m(a,m(m(q(b,c,d),e),g)),m(l(a,f),b)},v=function(a){for(var b,c=a.length,d=c+8,e=(d-d%64)/64,f=16*(e+1),g=new Array(f-1),h=0,i=0;c>i;)b=(i-i%4)/4,h=i%4*8,g[b]=g[b]|a.charCodeAt(i)<<h,i++;return b=(i-i%4)/4,h=i%4*8,g[b]=g[b]|128<<h,g[f-2]=c<<3,g[f-1]=c>>>29,g},w=function(a){var b,c,d="",e="";for(c=0;3>=c;c++)b=a>>>8*c&255,e="0"+b.toString(16),d+=e.substr(e.length-2,2);return d},x=[],y=7,z=12,A=17,B=22,C=5,D=9,E=14,F=20,G=4,H=11,I=16,J=23,K=6,L=10,M=15,N=21;for(x=v(a),h=1732584193,i=4023233417,j=2562383102,k=271733878,b=x.length,c=0;b>c;c+=16)d=h,e=i,f=j,g=k,h=r(h,i,j,k,x[c+0],y,3614090360),k=r(k,h,i,j,x[c+1],z,3905402710),j=r(j,k,h,i,x[c+2],A,606105819),i=r(i,j,k,h,x[c+3],B,3250441966),h=r(h,i,j,k,x[c+4],y,4118548399),k=r(k,h,i,j,x[c+5],z,1200080426),j=r(j,k,h,i,x[c+6],A,2821735955),i=r(i,j,k,h,x[c+7],B,4249261313),h=r(h,i,j,k,x[c+8],y,1770035416),k=r(k,h,i,j,x[c+9],z,2336552879),j=r(j,k,h,i,x[c+10],A,4294925233),i=r(i,j,k,h,x[c+11],B,2304563134),h=r(h,i,j,k,x[c+12],y,1804603682),k=r(k,h,i,j,x[c+13],z,4254626195),j=r(j,k,h,i,x[c+14],A,2792965006),i=r(i,j,k,h,x[c+15],B,1236535329),h=s(h,i,j,k,x[c+1],C,4129170786),k=s(k,h,i,j,x[c+6],D,3225465664),j=s(j,k,h,i,x[c+11],E,643717713),i=s(i,j,k,h,x[c+0],F,3921069994),h=s(h,i,j,k,x[c+5],C,3593408605),k=s(k,h,i,j,x[c+10],D,38016083),j=s(j,k,h,i,x[c+15],E,3634488961),i=s(i,j,k,h,x[c+4],F,3889429448),h=s(h,i,j,k,x[c+9],C,568446438),k=s(k,h,i,j,x[c+14],D,3275163606),j=s(j,k,h,i,x[c+3],E,4107603335),i=s(i,j,k,h,x[c+8],F,1163531501),h=s(h,i,j,k,x[c+13],C,2850285829),k=s(k,h,i,j,x[c+2],D,4243563512),j=s(j,k,h,i,x[c+7],E,1735328473),i=s(i,j,k,h,x[c+12],F,2368359562),h=t(h,i,j,k,x[c+5],G,4294588738),k=t(k,h,i,j,x[c+8],H,2272392833),j=t(j,k,h,i,x[c+11],I,1839030562),i=t(i,j,k,h,x[c+14],J,4259657740),h=t(h,i,j,k,x[c+1],G,2763975236),k=t(k,h,i,j,x[c+4],H,1272893353),j=t(j,k,h,i,x[c+7],I,4139469664),i=t(i,j,k,h,x[c+10],J,3200236656),h=t(h,i,j,k,x[c+13],G,681279174),k=t(k,h,i,j,x[c+0],H,3936430074),j=t(j,k,h,i,x[c+3],I,3572445317),i=t(i,j,k,h,x[c+6],J,76029189),h=t(h,i,j,k,x[c+9],G,3654602809),k=t(k,h,i,j,x[c+12],H,3873151461),j=t(j,k,h,i,x[c+15],I,530742520),i=t(i,j,k,h,x[c+2],J,3299628645),h=u(h,i,j,k,x[c+0],K,4096336452),k=u(k,h,i,j,x[c+7],L,1126891415),j=u(j,k,h,i,x[c+14],M,2878612391),i=u(i,j,k,h,x[c+5],N,4237533241),h=u(h,i,j,k,x[c+12],K,1700485571),k=u(k,h,i,j,x[c+3],L,2399980690),j=u(j,k,h,i,x[c+10],M,4293915773),i=u(i,j,k,h,x[c+1],N,2240044497),h=u(h,i,j,k,x[c+8],K,1873313359),k=u(k,h,i,j,x[c+15],L,4264355552),j=u(j,k,h,i,x[c+6],M,2734768916),i=u(i,j,k,h,x[c+13],N,1309151649),h=u(h,i,j,k,x[c+4],K,4149444226),k=u(k,h,i,j,x[c+11],L,3174756917),j=u(j,k,h,i,x[c+2],M,718787259),i=u(i,j,k,h,x[c+9],N,3951481745),h=m(h,d),i=m(i,e),j=m(j,f),k=m(k,g);var O=w(h)+w(i)+w(j)+w(k);return O.toLowerCase()}};return a}])}(this,this.angular,void 0);
|
||||
//# sourceMappingURL=angular-md5.min.js.map
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(E,d){'use strict';function y(t,l,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(b,e,a,c,k){function p(){m&&(g.cancel(m),m=null);h&&(h.$destroy(),h=null);n&&(m=g.leave(n),m.then(function(){m=null}),n=null)}function B(){var a=t.current&&t.current.locals;if(d.isDefined(a&&a.$template)){var a=b.$new(),c=t.current;n=k(a,function(a){g.enter(a,null,n||e).then(function(){!d.isDefined(A)||A&&!b.$eval(A)||l()});p()});h=c.scope=a;h.$emit("$viewContentLoaded");
|
||||
h.$eval(s)}else p()}var h,n,m,A=a.autoscroll,s=a.onload||"";b.$on("$routeChangeSuccess",B);B()}}}function w(d,l,g){return{restrict:"ECA",priority:-400,link:function(b,e){var a=g.current,c=a.locals;e.html(c.$template);var k=d(e.contents());if(a.controller){c.$scope=b;var p=l(a.controller,c);a.controllerAs&&(b[a.controllerAs]=p);e.data("$ngControllerController",p);e.children().data("$ngControllerController",p)}b[a.resolveAs||"$resolve"]=c;k(b)}}}var x,C,s=d.module("ngRoute",["ng"]).provider("$route",
|
||||
function(){function t(b,e){return d.extend(Object.create(b),e)}function l(b,d){var a=d.caseInsensitiveMatch,c={originalPath:b,regexp:b},g=c.keys=[];b=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g,function(b,a,d,c){b="?"===c||"*?"===c?"?":null;c="*"===c||"*?"===c?"*":null;g.push({name:d,optional:!!b});a=a||"";return""+(b?"":a)+"(?:"+(b?a:"")+(c&&"(.+?)"||"([^/]+)")+(b||"")+")"+(b||"")}).replace(/([\/$\*])/g,"\\$1");c.regexp=new RegExp("^"+b+"$",a?"i":"");return c}x=d.isArray;C=
|
||||
d.isObject;var g={};this.when=function(b,e){var a;a=void 0;if(x(e)){a=a||[];for(var c=0,k=e.length;c<k;c++)a[c]=e[c]}else if(C(e))for(c in a=a||{},e)if("$"!==c.charAt(0)||"$"!==c.charAt(1))a[c]=e[c];a=a||e;d.isUndefined(a.reloadOnSearch)&&(a.reloadOnSearch=!0);d.isUndefined(a.caseInsensitiveMatch)&&(a.caseInsensitiveMatch=this.caseInsensitiveMatch);g[b]=d.extend(a,b&&l(b,a));b&&(c="/"==b[b.length-1]?b.substr(0,b.length-1):b+"/",g[c]=d.extend({redirectTo:b},l(c,a)));return this};this.caseInsensitiveMatch=
|
||||
!1;this.otherwise=function(b){"string"===typeof b&&(b={redirectTo:b});this.when(null,b);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(b,e,a,c,k,p,l){function h(a){var f=v.current;(x=(r=y())&&f&&r.$$route===f.$$route&&d.equals(r.pathParams,f.pathParams)&&!r.reloadOnSearch&&!z)||!f&&!r||b.$broadcast("$routeChangeStart",r,f).defaultPrevented&&a&&a.preventDefault()}function n(){var u=v.current,f=r;if(x)u.params=f.params,d.copy(u.params,
|
||||
a),b.$broadcast("$routeUpdate",u);else if(f||u)z=!1,(v.current=f)&&f.redirectTo&&(d.isString(f.redirectTo)?e.path(w(f.redirectTo,f.params)).search(f.params).replace():e.url(f.redirectTo(f.pathParams,e.path(),e.search())).replace()),c.when(f).then(m).then(function(c){f==v.current&&(f&&(f.locals=c,d.copy(f.params,a)),b.$broadcast("$routeChangeSuccess",f,u))},function(a){f==v.current&&b.$broadcast("$routeChangeError",f,u,a)})}function m(a){if(a){var b=d.extend({},a.resolve);d.forEach(b,function(a,c){b[c]=
|
||||
d.isString(a)?k.get(a):k.invoke(a,null,null,c)});a=s(a);d.isDefined(a)&&(b.$template=a);return c.all(b)}}function s(a){var b,c;d.isDefined(b=a.template)?d.isFunction(b)&&(b=b(a.params)):d.isDefined(c=a.templateUrl)&&(d.isFunction(c)&&(c=c(a.params)),d.isDefined(c)&&(a.loadedTemplateUrl=l.valueOf(c),b=p(c)));return b}function y(){var a,b;d.forEach(g,function(c,g){var q;if(q=!b){var h=e.path();q=c.keys;var l={};if(c.regexp)if(h=c.regexp.exec(h)){for(var k=1,p=h.length;k<p;++k){var m=q[k-1],n=h[k];m&&
|
||||
n&&(l[m.name]=n)}q=l}else q=null;else q=null;q=a=q}q&&(b=t(c,{params:d.extend({},e.search(),a),pathParams:a}),b.$$route=c)});return b||g[null]&&t(g[null],{params:{},pathParams:{}})}function w(a,b){var c=[];d.forEach((a||"").split(":"),function(a,d){if(0===d)c.push(a);else{var e=a.match(/(\w+)(?:[?*])?(.*)/),g=e[1];c.push(b[g]);c.push(e[2]||"");delete b[g]}});return c.join("")}var z=!1,r,x,v={routes:g,reload:function(){z=!0;var a={defaultPrevented:!1,preventDefault:function(){this.defaultPrevented=
|
||||
!0;z=!1}};b.$evalAsync(function(){h(a);a.defaultPrevented||n()})},updateParams:function(a){if(this.current&&this.current.$$route)a=d.extend({},this.current.params,a),e.path(w(this.current.$$route.originalPath,a)),e.search(a);else throw D("norout");}};b.$on("$locationChangeStart",h);b.$on("$locationChangeSuccess",n);return v}]}),D=d.$$minErr("ngRoute");s.provider("$routeParams",function(){this.$get=function(){return{}}});s.directive("ngView",y);s.directive("ngView",w);y.$inject=["$route","$anchorScroll",
|
||||
"$animate"];w.$inject=["$compile","$controller","$route"]})(window,window.angular);
|
||||
//# sourceMappingURL=angular-route.min.js.map
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(s,g){'use strict';function H(g){var l=[];t(l,A).chars(g);return l.join("")}var B=g.$$minErr("$sanitize"),C,l,D,E,q,A,F,t;g.module("ngSanitize",[]).provider("$sanitize",function(){function k(a,e){var b={},c=a.split(","),h;for(h=0;h<c.length;h++)b[e?q(c[h]):c[h]]=!0;return b}function I(a){for(var e={},b=0,c=a.length;b<c;b++){var h=a[b];e[h.name]=h.value}return e}function G(a){return a.replace(/&/g,"&").replace(J,function(a){var b=a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(b-55296)+
|
||||
(a-56320)+65536)+";"}).replace(K,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}function u(a){if(a.nodeType===s.Node.ELEMENT_NODE)for(var e=a.attributes,b=0,c=e.length;b<c;b++){var h=e[b],d=h.name.toLowerCase();if("xmlns:ns1"===d||0===d.lastIndexOf("ns1:",0))a.removeAttributeNode(h),b--,c--}(e=a.firstChild)&&u(e);(e=a.nextSibling)&&u(e)}var v=!1;this.$get=["$$sanitizeUri",function(a){v&&l(w,x);return function(e){var b=[];F(e,t(b,function(b,h){return!/^unsafe:/.test(a(b,
|
||||
h))}));return b.join("")}}];this.enableSvg=function(a){return E(a)?(v=a,this):v};C=g.bind;l=g.extend;D=g.forEach;E=g.isDefined;q=g.lowercase;A=g.noop;F=function(a,e){null===a||void 0===a?a="":"string"!==typeof a&&(a=""+a);f.innerHTML=a;var b=5;do{if(0===b)throw B("uinput");b--;s.document.documentMode&&u(f);a=f.innerHTML;f.innerHTML=a}while(a!==f.innerHTML);for(b=f.firstChild;b;){switch(b.nodeType){case 1:e.start(b.nodeName.toLowerCase(),I(b.attributes));break;case 3:e.chars(b.textContent)}var c;if(!(c=
|
||||
b.firstChild)&&(1==b.nodeType&&e.end(b.nodeName.toLowerCase()),c=b.nextSibling,!c))for(;null==c;){b=b.parentNode;if(b===f)break;c=b.nextSibling;1==b.nodeType&&e.end(b.nodeName.toLowerCase())}b=c}for(;b=f.firstChild;)f.removeChild(b)};t=function(a,e){var b=!1,c=C(a,a.push);return{start:function(a,d){a=q(a);!b&&z[a]&&(b=a);b||!0!==w[a]||(c("<"),c(a),D(d,function(b,d){var f=q(d),g="img"===a&&"src"===f||"background"===f;!0!==m[f]||!0===n[f]&&!e(b,g)||(c(" "),c(d),c('="'),c(G(b)),c('"'))}),c(">"))},end:function(a){a=
|
||||
q(a);b||!0!==w[a]||!0===y[a]||(c("</"),c(a),c(">"));a==b&&(b=!1)},chars:function(a){b||c(G(a))}}};var J=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,K=/([^\#-~ |!])/g,y=k("area,br,col,hr,img,wbr"),d=k("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),r=k("rp,rt"),p=l({},r,d),d=l({},d,k("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")),r=l({},r,k("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
|
||||
x=k("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan"),z=k("script,style"),w=l({},y,d,r,p),n=k("background,cite,href,longdesc,src,xlink:href"),p=k("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,valign,value,vspace,width"),
|
||||
r=k("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan",
|
||||
!0),m=l({},n,r,p),f;(function(a){if(a.document&&a.document.implementation)a=a.document.implementation.createHTMLDocument("inert");else throw B("noinert");var e=(a.documentElement||a.getDocumentElement()).getElementsByTagName("body");1===e.length?f=e[0]:(e=a.createElement("html"),f=a.createElement("body"),e.appendChild(f),a.appendChild(e))})(s)});g.module("ngSanitize").filter("linky",["$sanitize",function(k){var l=/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
|
||||
q=/^mailto:/i,u=g.$$minErr("linky"),v=g.isDefined,s=g.isFunction,t=g.isObject,y=g.isString;return function(d,g,p){function x(a){a&&m.push(H(a))}function z(a,b){var c,d=w(a);m.push("<a ");for(c in d)m.push(c+'="'+d[c]+'" ');!v(g)||"target"in d||m.push('target="',g,'" ');m.push('href="',a.replace(/"/g,"""),'">');x(b);m.push("</a>")}if(null==d||""===d)return d;if(!y(d))throw u("notstring",d);for(var w=s(p)?p:t(p)?function(){return p}:function(){return{}},n=d,m=[],f,a;d=n.match(l);)f=d[0],d[2]||
|
||||
d[4]||(f=(d[3]?"http://":"mailto:")+f),a=d.index,x(n.substr(0,a)),z(f,d[0].replace(q,"")),n=n.substring(a+d[0].length);x(n);return k(m.join(""))}}])})(window,window.angular);
|
||||
//# sourceMappingURL=angular-sanitize.min.js.map
|
||||
@@ -1,156 +0,0 @@
|
||||
'use strict';
|
||||
angular.module('slick', []).directive('slick', [
|
||||
'$timeout',
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'AEC',
|
||||
scope: {
|
||||
initOnload: '@',
|
||||
data: '=',
|
||||
currentIndex: '=',
|
||||
accessibility: '@',
|
||||
adaptiveHeight: '@',
|
||||
arrows: '@',
|
||||
asNavFor: '@',
|
||||
appendArrows: '@',
|
||||
appendDots: '@',
|
||||
autoplay: '@',
|
||||
autoplaySpeed: '@',
|
||||
centerMode: '@',
|
||||
centerPadding: '@',
|
||||
cssEase: '@',
|
||||
customPaging: '&',
|
||||
dots: '@',
|
||||
draggable: '@',
|
||||
easing: '@',
|
||||
fade: '@',
|
||||
focusOnSelect: '@',
|
||||
infinite: '@',
|
||||
initialSlide: '@',
|
||||
lazyLoad: '@',
|
||||
onBeforeChange: '&',
|
||||
onAfterChange: '&',
|
||||
onInit: '&',
|
||||
onReInit: '&',
|
||||
onSetPosition: '&',
|
||||
pauseOnHover: '@',
|
||||
pauseOnDotsHover: '@',
|
||||
responsive: '=',
|
||||
rtl: '@',
|
||||
slide: '@',
|
||||
slidesToShow: '@',
|
||||
slidesToScroll: '@',
|
||||
speed: '@',
|
||||
swipe: '@',
|
||||
swipeToSlide: '@',
|
||||
touchMove: '@',
|
||||
touchThreshold: '@',
|
||||
useCSS: '@',
|
||||
variableWidth: '@',
|
||||
vertical: '@',
|
||||
prevArrow: '@',
|
||||
nextArrow: '@'
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
var destroySlick, initializeSlick, isInitialized;
|
||||
destroySlick = function () {
|
||||
return $timeout(function () {
|
||||
var slider;
|
||||
slider = $(element);
|
||||
slider.unslick();
|
||||
slider.find('.slick-list').remove();
|
||||
return slider;
|
||||
});
|
||||
};
|
||||
initializeSlick = function () {
|
||||
return $timeout(function () {
|
||||
var currentIndex, slider;
|
||||
slider = $(element);
|
||||
if (scope.currentIndex != null) {
|
||||
currentIndex = scope.currentIndex;
|
||||
}
|
||||
slider.slick({
|
||||
accessibility: scope.accessibility !== 'false',
|
||||
adaptiveHeight: scope.adaptiveHeight === 'true',
|
||||
arrows: scope.arrows !== 'false',
|
||||
asNavFor: scope.asNavFor ? scope.asNavFor : void 0,
|
||||
appendArrows: scope.appendArrows ? $(scope.appendArrows) : $(element),
|
||||
appendDots: scope.appendDots ? $(scope.appendDots) : $(element),
|
||||
autoplay: scope.autoplay === 'true',
|
||||
autoplaySpeed: scope.autoplaySpeed != null ? parseInt(scope.autoplaySpeed, 10) : 3000,
|
||||
centerMode: scope.centerMode === 'true',
|
||||
centerPadding: scope.centerPadding || '50px',
|
||||
cssEase: scope.cssEase || 'ease',
|
||||
customPaging: attrs.customPaging ? scope.customPaging : void 0,
|
||||
dots: scope.dots === 'true',
|
||||
draggable: scope.draggable !== 'false',
|
||||
easing: scope.easing || 'linear',
|
||||
fade: scope.fade === 'true',
|
||||
focusOnSelect: scope.focusOnSelect === 'true',
|
||||
infinite: scope.infinite !== 'false',
|
||||
initialSlide: scope.initialSlide || 0,
|
||||
lazyLoad: scope.lazyLoad || 'ondemand',
|
||||
onBeforeChange: attrs.onBeforeChange ? scope.onBeforeChange : void 0,
|
||||
onAfterChange: function (sl, index) {
|
||||
if (attrs.onAfterChange) {
|
||||
scope.onAfterChange();
|
||||
}
|
||||
if (currentIndex != null) {
|
||||
return scope.$apply(function () {
|
||||
currentIndex = index;
|
||||
return scope.currentIndex = index;
|
||||
});
|
||||
}
|
||||
},
|
||||
onInit: function (sl) {
|
||||
if (attrs.onInit) {
|
||||
scope.onInit();
|
||||
}
|
||||
if (currentIndex != null) {
|
||||
return sl.slideHandler(currentIndex);
|
||||
}
|
||||
},
|
||||
onReInit: attrs.onReInit ? scope.onReInit : void 0,
|
||||
onSetPosition: attrs.onSetPosition ? scope.onSetPosition : void 0,
|
||||
pauseOnHover: scope.pauseOnHover !== 'false',
|
||||
responsive: scope.responsive || void 0,
|
||||
rtl: scope.rtl === 'true',
|
||||
slide: scope.slide || 'div',
|
||||
slidesToShow: scope.slidesToShow != null ? parseInt(scope.slidesToShow, 10) : 1,
|
||||
slidesToScroll: scope.slidesToScroll != null ? parseInt(scope.slidesToScroll, 10) : 1,
|
||||
speed: scope.speed != null ? parseInt(scope.speed, 10) : 300,
|
||||
swipe: scope.swipe !== 'false',
|
||||
swipeToSlide: scope.swipeToSlide === 'true',
|
||||
touchMove: scope.touchMove !== 'false',
|
||||
touchThreshold: scope.touchThreshold ? parseInt(scope.touchThreshold, 10) : 5,
|
||||
useCSS: scope.useCSS !== 'false',
|
||||
variableWidth: scope.variableWidth === 'true',
|
||||
vertical: scope.vertical === 'true',
|
||||
prevArrow: scope.prevArrow ? $(scope.prevArrow) : void 0,
|
||||
nextArrow: scope.nextArrow ? $(scope.nextArrow) : void 0
|
||||
});
|
||||
return scope.$watch('currentIndex', function (newVal, oldVal) {
|
||||
if (currentIndex != null && newVal != null && newVal !== currentIndex) {
|
||||
return slider.slickGoTo(newVal);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
if (scope.initOnload) {
|
||||
isInitialized = false;
|
||||
return scope.$watch('data', function (newVal, oldVal) {
|
||||
if (newVal != null) {
|
||||
if (isInitialized) {
|
||||
destroySlick();
|
||||
}
|
||||
initializeSlick();
|
||||
return isInitialized = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return initializeSlick();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(e,i){"function"==typeof define&&define.amd?define([],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():i()}(0,function(){function e(n,a){"use strict";return function(r){if(!(r&&(angular.isArray(r.files)||angular.isString(r.prefix)&&angular.isString(r.suffix))))throw new Error("Couldn't load static files, no files and prefix or suffix specified!");r.files||(r.files=[{prefix:r.prefix,suffix:r.suffix}]);for(var e=function(e){if(!e||!angular.isString(e.prefix)||!angular.isString(e.suffix))throw new Error("Couldn't load static file, no prefix or suffix specified!");var i=[e.prefix,r.key,e.suffix].join("");return angular.isObject(r.fileMap)&&r.fileMap[i]&&(i=r.fileMap[i]),a(angular.extend({url:i,method:"GET"},r.$http)).then(function(e){return e.data},function(){return n.reject(r.key)})},i=[],t=r.files.length,f=0;f<t;f++)i.push(e({prefix:r.files[f].prefix,key:r.key,suffix:r.files[f].suffix}));return n.all(i).then(function(e){for(var i=e.length,r={},t=0;t<i;t++)for(var f in e[t])r[f]=e[t][f];return r})}}return e.$inject=["$q","$http"],angular.module("pascalprecht.translate").factory("$translateStaticFilesLoader",e),e.displayName="$translateStaticFilesLoader","pascalprecht.translate"});
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(t){"use strict";var n;if(1===angular.version.major&&4<=angular.version.minor){var o=t.get("$cookies");n={get:function(t){return o.get(t)},put:function(t,e){o.put(t,e)}}}else{var r=t.get("$cookieStore");n={get:function(t){return r.get(t)},put:function(t,e){r.put(t,e)}}}return{get:function(t){return n.get(t)},set:function(t,e){n.put(t,e)},put:function(t,e){n.put(t,e)}}}return t.$inject=["$injector"],angular.module("pascalprecht.translate").factory("$translateCookieStorage",t),t.displayName="$translateCookieStorage","pascalprecht.translate"});
|
||||
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* angular-translate - v2.18.3 - 2020-07-08
|
||||
*
|
||||
* Copyright (c) 2020 The angular-translate team, Pascal Precht; Licensed MIT
|
||||
*/
|
||||
!function(t,e){"function"==typeof define&&define.amd?define([],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():e()}(0,function(){function t(a,t){"use strict";var o,e={get:function(t){return o||(o=a.localStorage.getItem(t)),o},set:function(t,e){o=e,a.localStorage.setItem(t,e)},put:function(t,e){o=e,a.localStorage.setItem(t,e)}},r="localStorage"in a;if(r){var n="pascalprecht.translate.storageTest";try{r=null!==a.localStorage&&(a.localStorage.setItem(n,"foo"),a.localStorage.removeItem(n),!0)}catch(t){r=!1}}return r?e:t}return t.$inject=["$window","$translateCookieStorage"],angular.module("pascalprecht.translate").factory("$translateLocalStorage",t),t.displayName="$translateLocalStorageFactory","pascalprecht.translate"});
|
||||
@@ -1,271 +0,0 @@
|
||||
/**
|
||||
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
|
||||
* @author Alex_Crack
|
||||
* @version v0.3.6
|
||||
* @link https://github.com/alexcrack/angular-ui-notification
|
||||
* @license MIT
|
||||
*/
|
||||
angular.module('ui-notification', []);
|
||||
|
||||
angular.module('ui-notification').provider('Notification', function () {
|
||||
|
||||
this.options = {
|
||||
delay: 5000,
|
||||
startTop: 10,
|
||||
startRight: 10,
|
||||
verticalSpacing: 10,
|
||||
horizontalSpacing: 10,
|
||||
positionX: 'right',
|
||||
positionY: 'top',
|
||||
replaceMessage: false,
|
||||
templateUrl: 'angular-ui-notification.html',
|
||||
onClose: undefined,
|
||||
onClick: undefined,
|
||||
closeOnClick: true,
|
||||
maxCount: 0, // 0 - Infinite
|
||||
container: 'body',
|
||||
priority: 10
|
||||
};
|
||||
|
||||
this.setOptions = function (options) {
|
||||
if (!angular.isObject(options)) throw new Error("Options should be an object!");
|
||||
this.options = angular.extend({}, this.options, options);
|
||||
};
|
||||
|
||||
this.$get = ["$timeout", "$http", "$compile", "$templateCache", "$rootScope", "$injector", "$sce", "$q", "$window", function ($timeout, $http, $compile, $templateCache, $rootScope, $injector, $sce, $q, $window) {
|
||||
var options = this.options;
|
||||
|
||||
var startTop = options.startTop;
|
||||
var startRight = options.startRight;
|
||||
var verticalSpacing = options.verticalSpacing;
|
||||
var horizontalSpacing = options.horizontalSpacing;
|
||||
var delay = options.delay;
|
||||
|
||||
var messageElements = [];
|
||||
var isResizeBound = false;
|
||||
|
||||
var notify = function (args, t) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
if (typeof args !== 'object' || args === null) {
|
||||
args = {message: args};
|
||||
}
|
||||
|
||||
args.scope = args.scope ? args.scope : $rootScope;
|
||||
args.template = args.templateUrl ? args.templateUrl : options.templateUrl;
|
||||
args.delay = !angular.isUndefined(args.delay) ? args.delay : delay;
|
||||
args.type = t || args.type || options.type || '';
|
||||
args.positionY = args.positionY ? args.positionY : options.positionY;
|
||||
args.positionX = args.positionX ? args.positionX : options.positionX;
|
||||
args.replaceMessage = args.replaceMessage ? args.replaceMessage : options.replaceMessage;
|
||||
args.onClose = args.onClose ? args.onClose : options.onClose;
|
||||
args.onClick = args.onClick ? args.onClick : options.onClick;
|
||||
args.closeOnClick = (args.closeOnClick !== null && args.closeOnClick !== undefined) ? args.closeOnClick : options.closeOnClick;
|
||||
args.container = args.container ? args.container : options.container;
|
||||
args.priority = args.priority ? args.priority : options.priority;
|
||||
|
||||
var template = $templateCache.get(args.template);
|
||||
|
||||
if (template) {
|
||||
processNotificationTemplate(template);
|
||||
} else {
|
||||
// load it via $http only if it isn't default template and template isn't exist in template cache
|
||||
// cache:true means cache it for later access.
|
||||
$http.get(args.template, {cache: true})
|
||||
.then(function (response) {
|
||||
processNotificationTemplate(response.data);
|
||||
})
|
||||
.catch(function (data) {
|
||||
throw new Error('Template (' + args.template + ') could not be loaded. ' + data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function processNotificationTemplate(template) {
|
||||
|
||||
var scope = args.scope.$new();
|
||||
scope.message = $sce.trustAsHtml(args.message);
|
||||
scope.title = $sce.trustAsHtml(args.title);
|
||||
scope.t = args.type.substr(0, 1);
|
||||
scope.delay = args.delay;
|
||||
scope.onClose = args.onClose;
|
||||
scope.onClick = args.onClick;
|
||||
|
||||
var priorityCompareTop = function (a, b) {
|
||||
return a._priority - b._priority;
|
||||
};
|
||||
|
||||
var priorityCompareBtm = function (a, b) {
|
||||
return b._priority - a._priority;
|
||||
};
|
||||
|
||||
var reposite = function () {
|
||||
var j = 0;
|
||||
var k = 0;
|
||||
var lastTop = startTop;
|
||||
var lastRight = startRight;
|
||||
var lastPosition = [];
|
||||
|
||||
if (args.positionY === 'top') {
|
||||
messageElements.sort(priorityCompareTop);
|
||||
} else if (args.positionY === 'bottom') {
|
||||
messageElements.sort(priorityCompareBtm);
|
||||
}
|
||||
|
||||
for (var i = messageElements.length - 1; i >= 0; i--) {
|
||||
var element = messageElements[i];
|
||||
if (args.replaceMessage && i < messageElements.length - 1) {
|
||||
element.addClass('killed');
|
||||
continue;
|
||||
}
|
||||
var elHeight = parseInt(element[0].offsetHeight);
|
||||
var elWidth = parseInt(element[0].offsetWidth);
|
||||
var position = lastPosition[element._positionY + element._positionX];
|
||||
|
||||
if ((top + elHeight) > window.innerHeight) {
|
||||
position = startTop;
|
||||
k++;
|
||||
j = 0;
|
||||
}
|
||||
|
||||
var top = (lastTop = position ? (j === 0 ? position : position + verticalSpacing) : startTop);
|
||||
var right = lastRight + (k * (horizontalSpacing + elWidth));
|
||||
|
||||
element.css(element._positionY, top + 'px');
|
||||
if (element._positionX === 'center') {
|
||||
element.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
|
||||
} else {
|
||||
element.css(element._positionX, right + 'px');
|
||||
}
|
||||
|
||||
lastPosition[element._positionY + element._positionX] = top + elHeight;
|
||||
|
||||
if (options.maxCount > 0 && messageElements.length > options.maxCount && i === 0) {
|
||||
element.scope().kill(true);
|
||||
}
|
||||
|
||||
j++;
|
||||
}
|
||||
};
|
||||
|
||||
var templateElement = $compile(template)(scope);
|
||||
templateElement._positionY = args.positionY;
|
||||
templateElement._positionX = args.positionX;
|
||||
templateElement._priority = args.priority;
|
||||
templateElement.addClass(args.type);
|
||||
|
||||
var closeEvent = function (e) {
|
||||
e = e.originalEvent || e;
|
||||
if (e.type === 'click' || e.propertyName === 'opacity' && e.elapsedTime >= 1) {
|
||||
|
||||
if (scope.onClose) {
|
||||
scope.$apply(scope.onClose(templateElement));
|
||||
}
|
||||
|
||||
if (e.type === 'click')
|
||||
if (scope.onClick) {
|
||||
scope.$apply(scope.onClick(templateElement));
|
||||
}
|
||||
|
||||
templateElement.remove();
|
||||
messageElements.splice(messageElements.indexOf(templateElement), 1);
|
||||
scope.$destroy();
|
||||
reposite();
|
||||
}
|
||||
};
|
||||
|
||||
if (args.closeOnClick) {
|
||||
templateElement.addClass('clickable');
|
||||
templateElement.bind('click', closeEvent);
|
||||
}
|
||||
|
||||
templateElement.bind('webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd', closeEvent);
|
||||
|
||||
if (angular.isNumber(args.delay)) {
|
||||
$timeout(function () {
|
||||
templateElement.addClass('killed');
|
||||
}, args.delay);
|
||||
}
|
||||
|
||||
setCssTransitions('none');
|
||||
|
||||
angular.element(document.querySelector(args.container)).append(templateElement);
|
||||
var offset = -(parseInt(templateElement[0].offsetHeight) + 50);
|
||||
templateElement.css(templateElement._positionY, offset + "px");
|
||||
messageElements.push(templateElement);
|
||||
|
||||
if (args.positionX == 'center') {
|
||||
var elWidth = parseInt(templateElement[0].offsetWidth);
|
||||
templateElement.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
|
||||
}
|
||||
|
||||
$timeout(function () {
|
||||
setCssTransitions('');
|
||||
});
|
||||
|
||||
function setCssTransitions(value) {
|
||||
['-webkit-transition', '-o-transition', 'transition'].forEach(function (prefix) {
|
||||
templateElement.css(prefix, value);
|
||||
});
|
||||
}
|
||||
|
||||
scope._templateElement = templateElement;
|
||||
|
||||
scope.kill = function (isHard) {
|
||||
if (isHard) {
|
||||
if (scope.onClose) {
|
||||
scope.$apply(scope.onClose(scope._templateElement));
|
||||
}
|
||||
|
||||
messageElements.splice(messageElements.indexOf(scope._templateElement), 1);
|
||||
scope._templateElement.remove();
|
||||
scope.$destroy();
|
||||
$timeout(reposite);
|
||||
} else {
|
||||
scope._templateElement.addClass('killed');
|
||||
}
|
||||
};
|
||||
|
||||
$timeout(reposite);
|
||||
|
||||
if (!isResizeBound) {
|
||||
angular.element($window).bind('resize', function (e) {
|
||||
$timeout(reposite);
|
||||
});
|
||||
isResizeBound = true;
|
||||
}
|
||||
|
||||
deferred.resolve(scope);
|
||||
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
notify.primary = function (args) {
|
||||
return this(args, 'primary');
|
||||
};
|
||||
notify.error = function (args) {
|
||||
return this(args, 'error');
|
||||
};
|
||||
notify.success = function (args) {
|
||||
return this(args, 'success');
|
||||
};
|
||||
notify.info = function (args) {
|
||||
return this(args, 'info');
|
||||
};
|
||||
notify.warning = function (args) {
|
||||
return this(args, 'warning');
|
||||
};
|
||||
|
||||
notify.clearAll = function () {
|
||||
angular.forEach(messageElements, function (element) {
|
||||
element.addClass('killed');
|
||||
});
|
||||
};
|
||||
|
||||
return notify;
|
||||
}];
|
||||
});
|
||||
|
||||
angular.module("ui-notification").run(["$templateCache", function($templateCache) {$templateCache.put("angular-ui-notification.html","<div class=\"ui-notification\"><h3 ng-show=\"title\" ng-bind-html=\"title\"></h3><div class=\"message\" ng-bind-html=\"message\"></div></div>");}]);
|
||||
@@ -1,318 +0,0 @@
|
||||
/*
|
||||
AngularJS v1.5.8
|
||||
(c) 2010-2016 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(C){'use strict';function N(a){return function(){var b=arguments[0],d;d="["+(a?a+":":"")+b+"] http://errors.angularjs.org/1.5.8/"+(a?a+"/":"")+b;for(b=1;b<arguments.length;b++){d=d+(1==b?"?":"&")+"p"+(b-1)+"=";var c=encodeURIComponent,e;e=arguments[b];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;d+=c(e)}return Error(d)}}function ta(a){if(null==a||Va(a))return!1;if(L(a)||G(a)||F&&a instanceof F)return!0;
|
||||
var b="length"in Object(a)&&a.length;return T(b)&&(0<=b&&(b-1 in a||a instanceof Array)||"function"==typeof a.item)}function q(a,b,d){var c,e;if(a)if(z(a))for(c in a)"prototype"==c||"length"==c||"name"==c||a.hasOwnProperty&&!a.hasOwnProperty(c)||b.call(d,a[c],c,a);else if(L(a)||ta(a)){var f="object"!==typeof a;c=0;for(e=a.length;c<e;c++)(f||c in a)&&b.call(d,a[c],c,a)}else if(a.forEach&&a.forEach!==q)a.forEach(b,d,a);else if(sc(a))for(c in a)b.call(d,a[c],c,a);else if("function"===typeof a.hasOwnProperty)for(c in a)a.hasOwnProperty(c)&&
|
||||
b.call(d,a[c],c,a);else for(c in a)ua.call(a,c)&&b.call(d,a[c],c,a);return a}function tc(a,b,d){for(var c=Object.keys(a).sort(),e=0;e<c.length;e++)b.call(d,a[c[e]],c[e]);return c}function uc(a){return function(b,d){a(d,b)}}function Yd(){return++pb}function Pb(a,b,d){for(var c=a.$$hashKey,e=0,f=b.length;e<f;++e){var g=b[e];if(D(g)||z(g))for(var h=Object.keys(g),k=0,l=h.length;k<l;k++){var m=h[k],n=g[m];d&&D(n)?da(n)?a[m]=new Date(n.valueOf()):Wa(n)?a[m]=new RegExp(n):n.nodeName?a[m]=n.cloneNode(!0):
|
||||
Qb(n)?a[m]=n.clone():(D(a[m])||(a[m]=L(n)?[]:{}),Pb(a[m],[n],!0)):a[m]=n}}c?a.$$hashKey=c:delete a.$$hashKey;return a}function S(a){return Pb(a,va.call(arguments,1),!1)}function Zd(a){return Pb(a,va.call(arguments,1),!0)}function Z(a){return parseInt(a,10)}function Rb(a,b){return S(Object.create(a),b)}function A(){}function Xa(a){return a}function ha(a){return function(){return a}}function vc(a){return z(a.toString)&&a.toString!==ma}function y(a){return"undefined"===typeof a}function w(a){return"undefined"!==
|
||||
typeof a}function D(a){return null!==a&&"object"===typeof a}function sc(a){return null!==a&&"object"===typeof a&&!wc(a)}function G(a){return"string"===typeof a}function T(a){return"number"===typeof a}function da(a){return"[object Date]"===ma.call(a)}function z(a){return"function"===typeof a}function Wa(a){return"[object RegExp]"===ma.call(a)}function Va(a){return a&&a.window===a}function Ya(a){return a&&a.$evalAsync&&a.$watch}function Ga(a){return"boolean"===typeof a}function $d(a){return a&&T(a.length)&&
|
||||
ae.test(ma.call(a))}function Qb(a){return!(!a||!(a.nodeName||a.prop&&a.attr&&a.find))}function be(a){var b={};a=a.split(",");var d;for(d=0;d<a.length;d++)b[a[d]]=!0;return b}function wa(a){return Q(a.nodeName||a[0]&&a[0].nodeName)}function Za(a,b){var d=a.indexOf(b);0<=d&&a.splice(d,1);return d}function pa(a,b){function d(a,b){var d=b.$$hashKey,e;if(L(a)){e=0;for(var f=a.length;e<f;e++)b.push(c(a[e]))}else if(sc(a))for(e in a)b[e]=c(a[e]);else if(a&&"function"===typeof a.hasOwnProperty)for(e in a)a.hasOwnProperty(e)&&
|
||||
(b[e]=c(a[e]));else for(e in a)ua.call(a,e)&&(b[e]=c(a[e]));d?b.$$hashKey=d:delete b.$$hashKey;return b}function c(a){if(!D(a))return a;var b=f.indexOf(a);if(-1!==b)return g[b];if(Va(a)||Ya(a))throw xa("cpws");var b=!1,c=e(a);void 0===c&&(c=L(a)?[]:Object.create(wc(a)),b=!0);f.push(a);g.push(c);return b?d(a,c):c}function e(a){switch(ma.call(a)){case "[object Int8Array]":case "[object Int16Array]":case "[object Int32Array]":case "[object Float32Array]":case "[object Float64Array]":case "[object Uint8Array]":case "[object Uint8ClampedArray]":case "[object Uint16Array]":case "[object Uint32Array]":return new a.constructor(c(a.buffer),
|
||||
a.byteOffset,a.length);case "[object ArrayBuffer]":if(!a.slice){var b=new ArrayBuffer(a.byteLength);(new Uint8Array(b)).set(new Uint8Array(a));return b}return a.slice(0);case "[object Boolean]":case "[object Number]":case "[object String]":case "[object Date]":return new a.constructor(a.valueOf());case "[object RegExp]":return b=new RegExp(a.source,a.toString().match(/[^\/]*$/)[0]),b.lastIndex=a.lastIndex,b;case "[object Blob]":return new a.constructor([a],{type:a.type})}if(z(a.cloneNode))return a.cloneNode(!0)}
|
||||
var f=[],g=[];if(b){if($d(b)||"[object ArrayBuffer]"===ma.call(b))throw xa("cpta");if(a===b)throw xa("cpi");L(b)?b.length=0:q(b,function(a,d){"$$hashKey"!==d&&delete b[d]});f.push(a);g.push(b);return d(a,b)}return c(a)}function na(a,b){if(a===b)return!0;if(null===a||null===b)return!1;if(a!==a&&b!==b)return!0;var d=typeof a,c;if(d==typeof b&&"object"==d)if(L(a)){if(!L(b))return!1;if((d=a.length)==b.length){for(c=0;c<d;c++)if(!na(a[c],b[c]))return!1;return!0}}else{if(da(a))return da(b)?na(a.getTime(),
|
||||
b.getTime()):!1;if(Wa(a))return Wa(b)?a.toString()==b.toString():!1;if(Ya(a)||Ya(b)||Va(a)||Va(b)||L(b)||da(b)||Wa(b))return!1;d=U();for(c in a)if("$"!==c.charAt(0)&&!z(a[c])){if(!na(a[c],b[c]))return!1;d[c]=!0}for(c in b)if(!(c in d)&&"$"!==c.charAt(0)&&w(b[c])&&!z(b[c]))return!1;return!0}return!1}function $a(a,b,d){return a.concat(va.call(b,d))}function ab(a,b){var d=2<arguments.length?va.call(arguments,2):[];return!z(b)||b instanceof RegExp?b:d.length?function(){return arguments.length?b.apply(a,
|
||||
$a(d,arguments,0)):b.apply(a,d)}:function(){return arguments.length?b.apply(a,arguments):b.call(a)}}function ce(a,b){var d=b;"string"===typeof a&&"$"===a.charAt(0)&&"$"===a.charAt(1)?d=void 0:Va(b)?d="$WINDOW":b&&C.document===b?d="$DOCUMENT":Ya(b)&&(d="$SCOPE");return d}function bb(a,b){if(!y(a))return T(b)||(b=b?2:null),JSON.stringify(a,ce,b)}function xc(a){return G(a)?JSON.parse(a):a}function yc(a,b){a=a.replace(de,"");var d=Date.parse("Jan 01, 1970 00:00:00 "+a)/6E4;return isNaN(d)?b:d}function Sb(a,
|
||||
b,d){d=d?-1:1;var c=a.getTimezoneOffset();b=yc(b,c);d*=b-c;a=new Date(a.getTime());a.setMinutes(a.getMinutes()+d);return a}function ya(a){a=F(a).clone();try{a.empty()}catch(b){}var d=F("<div>").append(a).html();try{return a[0].nodeType===Ma?Q(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+Q(b)})}catch(c){return Q(d)}}function zc(a){try{return decodeURIComponent(a)}catch(b){}}function Ac(a){var b={};q((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),
|
||||
c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=zc(e),w(e)&&(f=w(f)?zc(f):!0,ua.call(b,e)?L(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Tb(a){var b=[];q(a,function(a,c){L(a)?q(a,function(a){b.push(ea(c,!0)+(!0===a?"":"="+ea(a,!0)))}):b.push(ea(c,!0)+(!0===a?"":"="+ea(a,!0)))});return b.length?b.join("&"):""}function qb(a){return ea(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ea(a,b){return encodeURIComponent(a).replace(/%40/gi,
|
||||
"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function ee(a,b){var d,c,e=Na.length;for(c=0;c<e;++c)if(d=Na[c]+b,G(d=a.getAttribute(d)))return d;return null}function fe(a,b){var d,c,e={};q(Na,function(b){b+="app";!d&&a.hasAttribute&&a.hasAttribute(b)&&(d=a,c=a.getAttribute(b))});q(Na,function(b){b+="app";var e;!d&&(e=a.querySelector("["+b.replace(":","\\:")+"]"))&&(d=e,c=e.getAttribute(b))});d&&(e.strictDi=null!==ee(d,"strict-di"),
|
||||
b(d,c?[c]:[],e))}function Bc(a,b,d){D(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=F(a);if(a.injector()){var c=a[0]===C.document?"document":ya(a);throw xa("btstrpd",c.replace(/</,"<").replace(/>/,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=cb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",
|
||||
d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;C&&e.test(C.name)&&(d.debugInfoEnabled=!0,C.name=C.name.replace(e,""));if(C&&!f.test(C.name))return c();C.name=C.name.replace(f,"");ca.resumeBootstrap=function(a){q(a,function(a){b.push(a)});return c()};z(ca.resumeDeferredBootstrap)&&ca.resumeDeferredBootstrap()}function ge(){C.name="NG_ENABLE_DEBUG_INFO!"+C.name;C.location.reload()}function he(a){a=ca.element(a).injector();if(!a)throw xa("test");return a.get("$$testability")}
|
||||
function Cc(a,b){b=b||"_";return a.replace(ie,function(a,c){return(c?b:"")+a.toLowerCase()})}function je(){var a;if(!Dc){var b=rb();(qa=y(b)?C.jQuery:b?C[b]:void 0)&&qa.fn.on?(F=qa,S(qa.fn,{scope:Oa.scope,isolateScope:Oa.isolateScope,controller:Oa.controller,injector:Oa.injector,inheritedData:Oa.inheritedData}),a=qa.cleanData,qa.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=qa._data(f,"events"))&&c.$destroy&&qa(f).triggerHandler("$destroy");a(b)}):F=O;ca.element=F;Dc=!0}}function sb(a,
|
||||
b,d){if(!a)throw xa("areq",b||"?",d||"required");return a}function Pa(a,b,d){d&&L(a)&&(a=a[a.length-1]);sb(z(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Qa(a,b){if("hasOwnProperty"===a)throw xa("badname",b);}function Ec(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g<f;g++)c=b[g],a&&(a=(e=a)[c]);return!d&&z(a)?ab(e,a):a}function tb(a){for(var b=a[0],d=a[a.length-1],c,e=1;b!==d&&(b=b.nextSibling);e++)if(c||a[e]!==
|
||||
b)c||(c=F(va.call(a,0,e))),c.push(b);return c||a}function U(){return Object.create(null)}function ke(a){function b(a,b,c){return a[b]||(a[b]=c())}var d=N("$injector"),c=N("ng");a=b(a,"angular",Object);a.$$minErr=a.$$minErr||N;return b(a,"module",function(){var a={};return function(f,g,h){if("hasOwnProperty"===f)throw c("badname","module");g&&a.hasOwnProperty(f)&&(a[f]=null);return b(a,f,function(){function a(b,d,e,f){f||(f=c);return function(){f[e||"push"]([b,d,arguments]);return R}}function b(a,
|
||||
d){return function(b,e){e&&z(e)&&(e.$$moduleName=f);c.push([a,d,arguments]);return R}}if(!g)throw d("nomod",f);var c=[],e=[],p=[],u=a("$injector","invoke","push",e),R={_invokeQueue:c,_configBlocks:e,_runBlocks:p,requires:g,name:f,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decorator:b("$provide","decorator"),animation:b("$animateProvider","register"),filter:b("$filterProvider",
|
||||
"register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider","directive"),component:b("$compileProvider","component"),config:u,run:function(a){p.push(a);return this}};h&&u(h);return R})}})}function ia(a,b){if(L(a)){b=b||[];for(var d=0,c=a.length;d<c;d++)b[d]=a[d]}else if(D(a))for(d in b=b||{},a)if("$"!==d.charAt(0)||"$"!==d.charAt(1))b[d]=a[d];return b||a}function le(a){S(a,{bootstrap:Bc,copy:pa,extend:S,merge:Zd,equals:na,element:F,forEach:q,injector:cb,noop:A,bind:ab,
|
||||
toJson:bb,fromJson:xc,identity:Xa,isUndefined:y,isDefined:w,isString:G,isFunction:z,isObject:D,isNumber:T,isElement:Qb,isArray:L,version:me,isDate:da,lowercase:Q,uppercase:ub,callbacks:{$$counter:0},getTestability:he,$$minErr:N,$$csp:Ba,reloadWithDebugInfo:ge});Ub=ke(C);Ub("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:ne});a.provider("$compile",Fc).directive({a:oe,input:Gc,textarea:Gc,form:pe,script:qe,select:re,style:se,option:te,ngBind:ue,ngBindHtml:ve,ngBindTemplate:we,ngClass:xe,
|
||||
ngClassEven:ye,ngClassOdd:ze,ngCloak:Ae,ngController:Be,ngForm:Ce,ngHide:De,ngIf:Ee,ngInclude:Fe,ngInit:Ge,ngNonBindable:He,ngPluralize:Ie,ngRepeat:Je,ngShow:Ke,ngStyle:Le,ngSwitch:Me,ngSwitchWhen:Ne,ngSwitchDefault:Oe,ngOptions:Pe,ngTransclude:Qe,ngModel:Re,ngList:Se,ngChange:Te,pattern:Hc,ngPattern:Hc,required:Ic,ngRequired:Ic,minlength:Jc,ngMinlength:Jc,maxlength:Kc,ngMaxlength:Kc,ngValue:Ue,ngModelOptions:Ve}).directive({ngInclude:We}).directive(vb).directive(Lc);a.provider({$anchorScroll:Xe,
|
||||
$animate:Ye,$animateCss:Ze,$$animateJs:$e,$$animateQueue:af,$$AnimateRunner:bf,$$animateAsyncRun:cf,$browser:df,$cacheFactory:ef,$controller:ff,$document:gf,$exceptionHandler:hf,$filter:Mc,$$forceReflow:jf,$interpolate:kf,$interval:lf,$http:mf,$httpParamSerializer:nf,$httpParamSerializerJQLike:of,$httpBackend:pf,$xhrFactory:qf,$jsonpCallbacks:rf,$location:sf,$log:tf,$parse:uf,$rootScope:vf,$q:wf,$$q:xf,$sce:yf,$sceDelegate:zf,$sniffer:Af,$templateCache:Bf,$templateRequest:Cf,$$testability:Df,$timeout:Ef,
|
||||
$window:Ff,$$rAF:Gf,$$jqLite:Hf,$$HashMap:If,$$cookieReader:Jf})}])}function db(a){return a.replace(Kf,function(a,d,c,e){return e?c.toUpperCase():c}).replace(Lf,"Moz$1")}function Nc(a){a=a.nodeType;return 1===a||!a||9===a}function Oc(a,b){var d,c,e=b.createDocumentFragment(),f=[];if(Vb.test(a)){d=e.appendChild(b.createElement("div"));c=(Mf.exec(a)||["",""])[1].toLowerCase();c=ja[c]||ja._default;d.innerHTML=c[1]+a.replace(Nf,"<$1></$2>")+c[2];for(c=c[0];c--;)d=d.lastChild;f=$a(f,d.childNodes);d=e.firstChild;
|
||||
d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";q(f,function(a){e.appendChild(a)});return e}function Pc(a,b){var d=a.parentNode;d&&d.replaceChild(b,a);b.appendChild(a)}function O(a){if(a instanceof O)return a;var b;G(a)&&(a=W(a),b=!0);if(!(this instanceof O)){if(b&&"<"!=a.charAt(0))throw Wb("nosel");return new O(a)}if(b){b=C.document;var d;a=(d=Of.exec(a))?[b.createElement(d[1])]:(d=Oc(a,b))?d.childNodes:[]}Qc(this,a)}function Xb(a){return a.cloneNode(!0)}function wb(a,
|
||||
b){b||eb(a);if(a.querySelectorAll)for(var d=a.querySelectorAll("*"),c=0,e=d.length;c<e;c++)eb(d[c])}function Rc(a,b,d,c){if(w(c))throw Wb("offargs");var e=(c=xb(a))&&c.events,f=c&&c.handle;if(f)if(b){var g=function(b){var c=e[b];w(d)&&Za(c||[],d);w(d)&&c&&0<c.length||(a.removeEventListener(b,f,!1),delete e[b])};q(b.split(" "),function(a){g(a);yb[a]&&g(yb[a])})}else for(b in e)"$destroy"!==b&&a.removeEventListener(b,f,!1),delete e[b]}function eb(a,b){var d=a.ng339,c=d&&fb[d];c&&(b?delete c.data[b]:
|
||||
(c.handle&&(c.events.$destroy&&c.handle({},"$destroy"),Rc(a)),delete fb[d],a.ng339=void 0))}function xb(a,b){var d=a.ng339,d=d&&fb[d];b&&!d&&(a.ng339=d=++Pf,d=fb[d]={events:{},data:{},handle:void 0});return d}function Yb(a,b,d){if(Nc(a)){var c=w(d),e=!c&&b&&!D(b),f=!b;a=(a=xb(a,!e))&&a.data;if(c)a[b]=d;else{if(f)return a;if(e)return a&&a[b];S(a,b)}}}function zb(a,b){return a.getAttribute?-1<(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+b+" "):!1}function Ab(a,b){b&&a.setAttribute&&
|
||||
q(b.split(" "),function(b){a.setAttribute("class",W((" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+W(b)+" "," ")))})}function Bb(a,b){if(b&&a.setAttribute){var d=(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");q(b.split(" "),function(a){a=W(a);-1===d.indexOf(" "+a+" ")&&(d+=a+" ")});a.setAttribute("class",W(d))}}function Qc(a,b){if(b)if(b.nodeType)a[a.length++]=b;else{var d=b.length;if("number"===typeof d&&b.window!==b){if(d)for(var c=0;c<d;c++)a[a.length++]=
|
||||
b[c]}else a[a.length++]=b}}function Sc(a,b){return Cb(a,"$"+(b||"ngController")+"Controller")}function Cb(a,b,d){9==a.nodeType&&(a=a.documentElement);for(b=L(b)?b:[b];a;){for(var c=0,e=b.length;c<e;c++)if(w(d=F.data(a,b[c])))return d;a=a.parentNode||11===a.nodeType&&a.host}}function Tc(a){for(wb(a,!0);a.firstChild;)a.removeChild(a.firstChild)}function Db(a,b){b||wb(a);var d=a.parentNode;d&&d.removeChild(a)}function Qf(a,b){b=b||C;if("complete"===b.document.readyState)b.setTimeout(a);else F(b).on("load",
|
||||
a)}function Uc(a,b){var d=Eb[b.toLowerCase()];return d&&Vc[wa(a)]&&d}function Rf(a,b){var d=function(c,d){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=b[d||c.type],g=f?f.length:0;if(g){if(y(c.immediatePropagationStopped)){var h=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=!0;c.stopPropagation&&c.stopPropagation();h&&h.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};var k=f.specialHandlerWrapper||
|
||||
Sf;1<g&&(f=ia(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||k(a,c,f[l])}};d.elem=a;return d}function Sf(a,b,d){d.call(a,b)}function Tf(a,b,d){var c=b.relatedTarget;c&&(c===a||Uf.call(a,c))||d.call(a,b)}function Hf(){this.$get=function(){return S(O,{hasClass:function(a,b){a.attr&&(a=a[0]);return zb(a,b)},addClass:function(a,b){a.attr&&(a=a[0]);return Bb(a,b)},removeClass:function(a,b){a.attr&&(a=a[0]);return Ab(a,b)}})}}function Ca(a,b){var d=a&&a.$$hashKey;if(d)return"function"===typeof d&&
|
||||
(d=a.$$hashKey()),d;d=typeof a;return d="function"==d||"object"==d&&null!==a?a.$$hashKey=d+":"+(b||Yd)():d+":"+a}function Ra(a,b){if(b){var d=0;this.nextUid=function(){return++d}}q(a,this.put,this)}function Wc(a){a=(Function.prototype.toString.call(a)+" ").replace(Vf,"");return a.match(Wf)||a.match(Xf)}function Yf(a){return(a=Wc(a))?"function("+(a[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function cb(a,b){function d(a){return function(b,c){if(D(b))q(b,uc(a));else return a(b,c)}}function c(a,b){Qa(a,
|
||||
"service");if(z(b)||L(b))b=p.instantiate(b);if(!b.$get)throw Ha("pget",a);return n[a+"Provider"]=b}function e(a,b){return function(){var c=B.invoke(b,this);if(y(c))throw Ha("undef",a);return c}}function f(a,b,d){return c(a,{$get:!1!==d?e(a,b):b})}function g(a){sb(y(a)||L(a),"modulesToLoad","not an array");var b=[],c;q(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=p.get(e[0]);f[e[1]].apply(f,e[2])}}if(!m.get(a)){m.put(a,!0);try{G(a)?(c=Ub(a),b=b.concat(g(c.requires)).concat(c._runBlocks),
|
||||
d(c._invokeQueue),d(c._configBlocks)):z(a)?b.push(p.invoke(a)):L(a)?b.push(p.invoke(a)):Pa(a,"module")}catch(e){throw L(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Ha("modulerr",a,e.stack||e.message||e);}}});return b}function h(a,c){function d(b,e){if(a.hasOwnProperty(b)){if(a[b]===k)throw Ha("cdep",b+" <- "+l.join(" <- "));return a[b]}try{return l.unshift(b),a[b]=k,a[b]=c(b,e)}catch(f){throw a[b]===k&&delete a[b],f;}finally{l.shift()}}function e(a,
|
||||
c,f){var g=[];a=cb.$$annotate(a,b,f);for(var h=0,k=a.length;h<k;h++){var l=a[h];if("string"!==typeof l)throw Ha("itkn",l);g.push(c&&c.hasOwnProperty(l)?c[l]:d(l,f))}return g}return{invoke:function(a,b,c,d){"string"===typeof c&&(d=c,c=null);c=e(a,c,d);L(a)&&(a=a[a.length-1]);d=11>=Ea?!1:"function"===typeof a&&/^(?:class\b|constructor\()/.test(Function.prototype.toString.call(a)+" ");return d?(c.unshift(null),new (Function.prototype.bind.apply(a,c))):a.apply(b,c)},instantiate:function(a,b,c){var d=
|
||||
L(a)?a[a.length-1]:a;a=e(a,b,c);a.unshift(null);return new (Function.prototype.bind.apply(d,a))},get:d,annotate:cb.$$annotate,has:function(b){return n.hasOwnProperty(b+"Provider")||a.hasOwnProperty(b)}}}b=!0===b;var k={},l=[],m=new Ra([],!0),n={$provide:{provider:d(c),factory:d(f),service:d(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:d(function(a,b){return f(a,ha(b),!1)}),constant:d(function(a,b){Qa(a,"constant");n[a]=b;u[a]=b}),decorator:function(a,b){var c=
|
||||
p.get(a+"Provider"),d=c.$get;c.$get=function(){var a=B.invoke(d,c);return B.invoke(b,null,{$delegate:a})}}}},p=n.$injector=h(n,function(a,b){ca.isString(b)&&l.push(b);throw Ha("unpr",l.join(" <- "));}),u={},R=h(u,function(a,b){var c=p.get(a+"Provider",b);return B.invoke(c.$get,c,void 0,a)}),B=R;n.$injectorProvider={$get:ha(R)};var r=g(a),B=R.get("$injector");B.strictDi=b;q(r,function(a){a&&B.invoke(a)});return B}function Xe(){var a=!0;this.disableAutoScrolling=function(){a=!1};this.$get=["$window",
|
||||
"$location","$rootScope",function(b,d,c){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===wa(a))return b=a,!0});return b}function f(a){if(a){a.scrollIntoView();var c;c=g.yOffset;z(c)?c=c():Qb(c)?(c=c[0],c="fixed"!==b.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):T(c)||(c=0);c&&(a=a.getBoundingClientRect().top,b.scrollBy(0,a-c))}else b.scrollTo(0,0)}function g(a){a=G(a)?a:d.hash();var b;a?(b=h.getElementById(a))?f(b):(b=e(h.getElementsByName(a)))?f(b):"top"===
|
||||
a&&f(null):f(null)}var h=b.document;a&&c.$watch(function(){return d.hash()},function(a,b){a===b&&""===a||Qf(function(){c.$evalAsync(g)})});return g}]}function gb(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;L(a)&&(a=a.join(" "));L(b)&&(b=b.join(" "));return a+" "+b}function Zf(a){G(a)&&(a=a.split(" "));var b=U();q(a,function(a){a.length&&(b[a]=!0)});return b}function Ia(a){return D(a)?a:{}}function $f(a,b,d,c){function e(a){try{a.apply(null,va.call(arguments,1))}finally{if(R--,0===R)for(;B.length;)try{B.pop()()}catch(b){d.error(b)}}}
|
||||
function f(){t=null;g();h()}function g(){r=K();r=y(r)?null:r;na(r,E)&&(r=E);E=r}function h(){if(v!==k.url()||J!==r)v=k.url(),J=r,q(M,function(a){a(k.url(),r)})}var k=this,l=a.location,m=a.history,n=a.setTimeout,p=a.clearTimeout,u={};k.isMock=!1;var R=0,B=[];k.$$completeOutstandingRequest=e;k.$$incOutstandingRequestCount=function(){R++};k.notifyWhenNoOutstandingRequests=function(a){0===R?a():B.push(a)};var r,J,v=l.href,fa=b.find("base"),t=null,K=c.history?function(){try{return m.state}catch(a){}}:
|
||||
A;g();J=r;k.url=function(b,d,e){y(e)&&(e=null);l!==a.location&&(l=a.location);m!==a.history&&(m=a.history);if(b){var f=J===e;if(v===b&&(!c.history||f))return k;var h=v&&Ja(v)===Ja(b);v=b;J=e;!c.history||h&&f?(h||(t=b),d?l.replace(b):h?(d=l,e=b.indexOf("#"),e=-1===e?"":b.substr(e),d.hash=e):l.href=b,l.href!==b&&(t=b)):(m[d?"replaceState":"pushState"](e,"",b),g(),J=r);t&&(t=b);return k}return t||l.href.replace(/%27/g,"'")};k.state=function(){return r};var M=[],H=!1,E=null;k.onUrlChange=function(b){if(!H){if(c.history)F(a).on("popstate",
|
||||
f);F(a).on("hashchange",f);H=!0}M.push(b);return b};k.$$applicationDestroyed=function(){F(a).off("hashchange popstate",f)};k.$$checkUrlChange=h;k.baseHref=function(){var a=fa.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};k.defer=function(a,b){var c;R++;c=n(function(){delete u[c];e(a)},b||0);u[c]=!0;return c};k.defer.cancel=function(a){return u[a]?(delete u[a],p(a),e(A),!0):!1}}function df(){this.$get=["$window","$log","$sniffer","$document",function(a,b,d,c){return new $f(a,c,b,
|
||||
d)}]}function ef(){this.$get=function(){function a(a,c){function e(a){a!=n&&(p?p==a&&(p=a.n):p=a,f(a.n,a.p),f(a,n),n=a,n.n=null)}function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(a in b)throw N("$cacheFactory")("iid",a);var g=0,h=S({},c,{id:a}),k=U(),l=c&&c.capacity||Number.MAX_VALUE,m=U(),n=null,p=null;return b[a]={put:function(a,b){if(!y(b)){if(l<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}a in k||g++;k[a]=b;g>l&&this.remove(p.key);return b}},get:function(a){if(l<Number.MAX_VALUE){var b=m[a];
|
||||
if(!b)return;e(b)}return k[a]},remove:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;b==n&&(n=b.p);b==p&&(p=b.n);f(b.n,b.p);delete m[a]}a in k&&(delete k[a],g--)},removeAll:function(){k=U();g=0;m=U();n=p=null},destroy:function(){m=h=k=null;delete b[a]},info:function(){return S({},h,{size:g})}}}var b={};a.info=function(){var a={};q(b,function(b,e){a[e]=b.info()});return a};a.get=function(a){return b[a]};return a}}function Bf(){this.$get=["$cacheFactory",function(a){return a("templates")}]}
|
||||
function Fc(a,b){function d(a,b,c){var d=/^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/,e=U();q(a,function(a,f){if(a in n)e[f]=n[a];else{var g=a.match(d);if(!g)throw ga("iscp",b,f,a,c?"controller bindings definition":"isolate scope definition");e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f};g[4]&&(n[a]=e[f])}});return e}function c(a){var b=a.charAt(0);if(!b||b!==Q(b))throw ga("baddir",a);if(a!==a.trim())throw ga("baddir",a);}function e(a){var b=a.require||a.controller&&a.name;
|
||||
!L(b)&&D(b)&&q(b,function(a,c){var d=a.match(l);a.substring(d[0].length)||(b[c]=d[0]+c)});return b}var f={},g=/^\s*directive\:\s*([\w\-]+)\s+(.*)$/,h=/(([\w\-]+)(?:\:([^;]+))?;?)/,k=be("ngSrc,ngSrcset,src,srcset"),l=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,m=/^(on[a-z]+|formaction)$/,n=U();this.directive=function B(b,d){Qa(b,"directive");G(b)?(c(b),sb(d,"directiveFactory"),f.hasOwnProperty(b)||(f[b]=[],a.factory(b+"Directive",["$injector","$exceptionHandler",function(a,c){var d=[];q(f[b],function(f,g){try{var h=
|
||||
a.invoke(f);z(h)?h={compile:ha(h)}:!h.compile&&h.link&&(h.compile=ha(h.link));h.priority=h.priority||0;h.index=g;h.name=h.name||b;h.require=e(h);h.restrict=h.restrict||"EA";h.$$moduleName=f.$$moduleName;d.push(h)}catch(k){c(k)}});return d}])),f[b].push(d)):q(b,uc(B));return this};this.component=function(a,b){function c(a){function e(b){return z(b)||L(b)?function(c,d){return a.invoke(b,this,{$element:c,$attrs:d})}:b}var f=b.template||b.templateUrl?b.template:"",g={controller:d,controllerAs:Xc(b.controller)||
|
||||
b.controllerAs||"$ctrl",template:e(f),templateUrl:e(b.templateUrl),transclude:b.transclude,scope:{},bindToController:b.bindings||{},restrict:"E",require:b.require};q(b,function(a,b){"$"===b.charAt(0)&&(g[b]=a)});return g}var d=b.controller||function(){};q(b,function(a,b){"$"===b.charAt(0)&&(c[b]=a,z(d)&&(d[b]=a))});c.$inject=["$injector"];return this.directive(a,c)};this.aHrefSanitizationWhitelist=function(a){return w(a)?(b.aHrefSanitizationWhitelist(a),this):b.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=
|
||||
function(a){return w(a)?(b.imgSrcSanitizationWhitelist(a),this):b.imgSrcSanitizationWhitelist()};var p=!0;this.debugInfoEnabled=function(a){return w(a)?(p=a,this):p};var u=10;this.onChangesTtl=function(a){return arguments.length?(u=a,this):u};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$sce","$animate","$$sanitizeUri",function(a,b,c,e,n,t,K,M,H,E){function I(){try{if(!--qa)throw Y=void 0,ga("infchng",u);K.$apply(function(){for(var a=
|
||||
[],b=0,c=Y.length;b<c;++b)try{Y[b]()}catch(d){a.push(d)}Y=void 0;if(a.length)throw a;})}finally{qa++}}function Da(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a}function P(a,b,c){pa.innerHTML="<span "+b+">";b=pa.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function x(a,b){try{a.addClass(b)}catch(c){}}function aa(a,b,c,d,e){a instanceof F||(a=F(a));for(var f=/\S+/,g=0,h=a.length;g<
|
||||
h;g++){var k=a[g];k.nodeType===Ma&&k.nodeValue.match(f)&&Pc(k,a[g]=C.document.createElement("span"))}var l=s(a,b,a,c,d,e);aa.$$addScopeClass(a);var m=null;return function(b,c,d){sb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var f=d.parentBoundTranscludeFn,g=d.transcludeControllers;d=d.futureParentElement;f&&f.$$boundTransclude&&(f=f.$$boundTransclude);m||(m=(d=d&&d[0])?"foreignobject"!==wa(d)&&ma.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==m?F(da(m,F("<div>").append(a).html())):
|
||||
c?Oa.clone.call(a):a;if(g)for(var h in g)d.data("$"+h+"Controller",g[h].instance);aa.$$addScopeInfo(d,b);c&&c(d,b);l&&l(b,d,d,f);return d}}function s(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,p,r,v;if(n)for(v=Array(c.length),m=0;m<h.length;m+=3)f=h[m],v[f]=c[f];else v=c;m=0;for(p=h.length;m<p;)k=v[h[m++]],c=h[m++],f=h[m++],c?(c.scope?(l=a.$new(),aa.$$addScopeInfo(F(k),l)):l=a,r=c.transcludeOnThisElement?za(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?za(a,b):null,c(f,l,k,d,r)):f&&f(a,
|
||||
k.childNodes,void 0,e)}for(var h=[],k,l,m,p,n,r=0;r<a.length;r++){k=new Da;l=$b(a[r],[],k,0===r?d:void 0,e);(f=l.length?oa(l,a[r],k,b,c,null,[],[],f):null)&&f.scope&&aa.$$addScopeClass(k.$$element);k=f&&f.terminal||!(m=a[r].childNodes)||!m.length?null:s(m,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||k)h.push(r,f,k),p=!0,n=n||f;f=null}return p?g:null}function za(a,b,c){function d(e,f,g,h,k){e||(e=a.$new(!1,k),e.$$transcluded=!0);return b(e,f,{parentBoundTranscludeFn:c,
|
||||
transcludeControllers:g,futureParentElement:h})}var e=d.$$slots=U(),f;for(f in b.$$slots)e[f]=b.$$slots[f]?za(a,b.$$slots[f],c):null;return d}function $b(a,b,c,d,e){var f=c.$attr;switch(a.nodeType){case 1:O(b,Aa(wa(a)),"E",d,e);for(var g,k,l,m,p=a.attributes,n=0,r=p&&p.length;n<r;n++){var v=!1,u=!1;g=p[n];k=g.name;l=W(g.value);g=Aa(k);if(m=Ba.test(g))k=k.replace(Yc,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()});(g=g.match(Ca))&&V(g[1])&&(v=k,u=k.substr(0,k.length-5)+"end",k=
|
||||
k.substr(0,k.length-6));g=Aa(k.toLowerCase());f[g]=k;if(m||!c.hasOwnProperty(g))c[g]=l,Uc(a,g)&&(c[g]=!0);ia(a,b,l,g,m);O(b,g,"A",d,e,v,u)}f=a.className;D(f)&&(f=f.animVal);if(G(f)&&""!==f)for(;a=h.exec(f);)g=Aa(a[2]),O(b,g,"C",d,e)&&(c[g]=W(a[3])),f=f.substr(a.index+a[0].length);break;case Ma:if(11===Ea)for(;a.parentNode&&a.nextSibling&&a.nextSibling.nodeType===Ma;)a.nodeValue+=a.nextSibling.nodeValue,a.parentNode.removeChild(a.nextSibling);ca(b,a.nodeValue);break;case 8:hb(a,b,c,d,e)}b.sort(Z);
|
||||
return b}function hb(a,b,c,d,e){try{var f=g.exec(a.nodeValue);if(f){var h=Aa(f[1]);O(b,h,"M",d,e)&&(c[h]=W(f[2]))}}catch(k){}}function N(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw ga("uterdir",b,c);1==a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return F(d)}function Zc(a,b,c){return function(d,e,f,g,h){e=N(e[0],b,c);return a(d,e,f,g,h)}}function ac(a,b,c,d,e,f){var g;return a?aa(b,c,d,e,f):function(){g||
|
||||
(g=aa(b,c,d,e,f),b=c=f=null);return g.apply(this,arguments)}}function oa(a,b,d,e,f,g,h,k,l){function m(a,b,c,d){if(a){c&&(a=Zc(a,c,d));a.require=x.require;a.directiveName=I;if(u===x||x.$$isolateScope)a=ja(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=Zc(b,c,d));b.require=x.require;b.directiveName=I;if(u===x||x.$$isolateScope)b=ja(b,{isolateScope:!0});k.push(b)}}function p(a,e,f,g,l){function m(a,b,c,d){var e;Ya(a)||(d=c,c=b,b=a,a=void 0);fa&&(e=t);c||(c=fa?I.parent():I);if(d){var f=l.$$slots[d];if(f)return f(a,
|
||||
b,e,c,s);if(y(f))throw ga("noslot",d,ya(I));}else return l(a,b,e,c,s)}var n,E,x,M,B,t,P,I;b===f?(g=d,I=d.$$element):(I=F(f),g=new Da(I,d));B=e;u?M=e.$new(!0):r&&(B=e.$parent);l&&(P=m,P.$$boundTransclude=l,P.isSlotFilled=function(a){return!!l.$$slots[a]});v&&(t=ag(I,g,P,v,M,e,u));u&&(aa.$$addScopeInfo(I,M,!0,!(H&&(H===u||H===u.$$originalDirective))),aa.$$addScopeClass(I,!0),M.$$isolateBindings=u.$$isolateBindings,E=ka(e,g,M,M.$$isolateBindings,u),E.removeWatches&&M.$on("$destroy",E.removeWatches));
|
||||
for(n in t){E=v[n];x=t[n];var Zb=E.$$bindings.bindToController;x.bindingInfo=x.identifier&&Zb?ka(B,g,x.instance,Zb,E):{};var K=x();K!==x.instance&&(x.instance=K,I.data("$"+E.name+"Controller",K),x.bindingInfo.removeWatches&&x.bindingInfo.removeWatches(),x.bindingInfo=ka(B,g,x.instance,Zb,E))}q(v,function(a,b){var c=a.require;a.bindToController&&!L(c)&&D(c)&&S(t[b].instance,ib(b,c,I,t))});q(t,function(a){var b=a.instance;if(z(b.$onChanges))try{b.$onChanges(a.bindingInfo.initialChanges)}catch(d){c(d)}if(z(b.$onInit))try{b.$onInit()}catch(e){c(e)}z(b.$doCheck)&&
|
||||
(B.$watch(function(){b.$doCheck()}),b.$doCheck());z(b.$onDestroy)&&B.$on("$destroy",function(){b.$onDestroy()})});n=0;for(E=h.length;n<E;n++)x=h[n],la(x,x.isolateScope?M:e,I,g,x.require&&ib(x.directiveName,x.require,I,t),P);var s=e;u&&(u.template||null===u.templateUrl)&&(s=M);a&&a(s,f.childNodes,void 0,l);for(n=k.length-1;0<=n;n--)x=k[n],la(x,x.isolateScope?M:e,I,g,x.require&&ib(x.directiveName,x.require,I,t),P);q(t,function(a){a=a.instance;z(a.$postLink)&&a.$postLink()})}l=l||{};for(var n=-Number.MAX_VALUE,
|
||||
r=l.newScopeDirective,v=l.controllerDirectives,u=l.newIsolateScopeDirective,H=l.templateDirective,E=l.nonTlbTranscludeDirective,M=!1,B=!1,fa=l.hasElementTranscludeDirective,t=d.$$element=F(b),x,I,P,K=e,s,Fa=!1,za=!1,w,A=0,C=a.length;A<C;A++){x=a[A];var G=x.$$start,hb=x.$$end;G&&(t=N(b,G,hb));P=void 0;if(n>x.priority)break;if(w=x.scope)x.templateUrl||(D(w)?(X("new/isolated scope",u||r,x,t),u=x):X("new/isolated scope",u,x,t)),r=r||x;I=x.name;if(!Fa&&(x.replace&&(x.templateUrl||x.template)||x.transclude&&
|
||||
!x.$$tlb)){for(w=A+1;Fa=a[w++];)if(Fa.transclude&&!Fa.$$tlb||Fa.replace&&(Fa.templateUrl||Fa.template)){za=!0;break}Fa=!0}!x.templateUrl&&x.controller&&(w=x.controller,v=v||U(),X("'"+I+"' controller",v[I],x,t),v[I]=x);if(w=x.transclude)if(M=!0,x.$$tlb||(X("transclusion",E,x,t),E=x),"element"==w)fa=!0,n=x.priority,P=t,t=d.$$element=F(aa.$$createComment(I,d[I])),b=t[0],ea(f,va.call(P,0),b),P[0].$$parentNode=P[0].parentNode,K=ac(za,P,e,n,g&&g.name,{nonTlbTranscludeDirective:E});else{var oa=U();P=F(Xb(b)).contents();
|
||||
if(D(w)){P=[];var Q=U(),O=U();q(w,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Q[a]=b;oa[b]=null;O[b]=c});q(t.contents(),function(a){var b=Q[Aa(wa(a))];b?(O[b]=!0,oa[b]=oa[b]||[],oa[b].push(a)):P.push(a)});q(O,function(a,b){if(!a)throw ga("reqslot",b);});for(var V in oa)oa[V]&&(oa[V]=ac(za,oa[V],e))}t.empty();K=ac(za,P,e,void 0,void 0,{needsNewScope:x.$$isolateScope||x.$$newScope});K.$$slots=oa}if(x.template)if(B=!0,X("template",H,x,t),H=x,w=z(x.template)?x.template(t,d):x.template,
|
||||
w=xa(w),x.replace){g=x;P=Vb.test(w)?$c(da(x.templateNamespace,W(w))):[];b=P[0];if(1!=P.length||1!==b.nodeType)throw ga("tplrt",I,"");ea(f,t,b);C={$attr:{}};w=$b(b,[],C);var Z=a.splice(A+1,a.length-(A+1));(u||r)&&T(w,u,r);a=a.concat(w).concat(Z);$(d,C);C=a.length}else t.html(w);if(x.templateUrl)B=!0,X("template",H,x,t),H=x,x.replace&&(g=x),p=ba(a.splice(A,a.length-A),t,d,f,M&&K,h,k,{controllerDirectives:v,newScopeDirective:r!==x&&r,newIsolateScopeDirective:u,templateDirective:H,nonTlbTranscludeDirective:E}),
|
||||
C=a.length;else if(x.compile)try{s=x.compile(t,d,K);var Y=x.$$originalDirective||x;z(s)?m(null,ab(Y,s),G,hb):s&&m(ab(Y,s.pre),ab(Y,s.post),G,hb)}catch(ca){c(ca,ya(t))}x.terminal&&(p.terminal=!0,n=Math.max(n,x.priority))}p.scope=r&&!0===r.scope;p.transcludeOnThisElement=M;p.templateOnThisElement=B;p.transclude=K;l.hasElementTranscludeDirective=fa;return p}function ib(a,b,c,d){var e;if(G(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&
|
||||
e.instance;if(!e){var h="$"+b+"Controller";e=g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw ga("ctreq",b,a);}else if(L(b))for(e=[],g=0,f=b.length;g<f;g++)e[g]=ib(a,b[g],c,d);else D(b)&&(e={},q(b,function(b,f){e[f]=ib(a,b,c,d)}));return e||null}function ag(a,b,c,d,e,f,g){var h=U(),k;for(k in d){var l=d[k],m={$scope:l===g||l.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},p=l.controller;"@"==p&&(p=b[l.name]);m=t(p,m,!0,l.controllerAs);h[l.name]=m;a.data("$"+l.name+"Controller",m.instance)}return h}
|
||||
function T(a,b,c){for(var d=0,e=a.length;d<e;d++)a[d]=Rb(a[d],{$$isolateScope:b,$$newScope:c})}function O(b,e,g,h,k,l,m){if(e===k)return null;k=null;if(f.hasOwnProperty(e)){var p;e=a.get(e+"Directive");for(var n=0,r=e.length;n<r;n++)try{if(p=e[n],(y(h)||h>p.priority)&&-1!=p.restrict.indexOf(g)){l&&(p=Rb(p,{$$start:l,$$end:m}));if(!p.$$bindings){var u=p,v=p,x=p.name,H={isolateScope:null,bindToController:null};D(v.scope)&&(!0===v.bindToController?(H.bindToController=d(v.scope,x,!0),H.isolateScope={}):
|
||||
H.isolateScope=d(v.scope,x,!1));D(v.bindToController)&&(H.bindToController=d(v.bindToController,x,!0));if(D(H.bindToController)){var E=v.controller,M=v.controllerAs;if(!E)throw ga("noctrl",x);if(!Xc(E,M))throw ga("noident",x);}var t=u.$$bindings=H;D(t.isolateScope)&&(p.$$isolateBindings=t.isolateScope)}b.push(p);k=p}}catch(I){c(I)}}return k}function V(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d<e;d++)if(b=c[d],b.multiElement)return!0;return!1}function $(a,b){var c=b.$attr,
|
||||
d=a.$attr;q(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});q(b,function(b,e){a.hasOwnProperty(e)||"$"===e.charAt(0)||(a[e]=b,"class"!==e&&"style"!==e&&(d[e]=c[e]))})}function ba(a,b,c,d,f,g,h,k){var l=[],m,p,n=b[0],r=a.shift(),u=Rb(r,{templateUrl:null,transclude:null,replace:null,$$originalDirective:r}),H=z(r.templateUrl)?r.templateUrl(b,c):r.templateUrl,E=r.templateNamespace;b.empty();e(H).then(function(e){var v,M;e=xa(e);if(r.replace){e=
|
||||
Vb.test(e)?$c(da(E,W(e))):[];v=e[0];if(1!=e.length||1!==v.nodeType)throw ga("tplrt",r.name,H);e={$attr:{}};ea(d,b,v);var B=$b(v,[],e);D(r.scope)&&T(B,!0);a=B.concat(a);$(c,e)}else v=n,b.html(e);a.unshift(u);m=oa(a,v,c,f,b,r,g,h,k);q(d,function(a,c){a==v&&(d[c]=b[0])});for(p=s(b[0].childNodes,f);l.length;){e=l.shift();M=l.shift();var t=l.shift(),I=l.shift(),B=b[0];if(!e.$$destroyed){if(M!==n){var P=M.className;k.hasElementTranscludeDirective&&r.replace||(B=Xb(v));ea(t,F(M),B);x(F(B),P)}M=m.transcludeOnThisElement?
|
||||
za(e,m.transclude,I):I;m(p,e,B,d,M)}}l=null});return function(a,b,c,d,e){a=e;b.$$destroyed||(l?l.push(b,c,d,a):(m.transcludeOnThisElement&&(a=za(b,m.transclude,e)),m(p,b,c,d,a)))}}function Z(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function X(a,b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw ga("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),a,ya(d));}function ca(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=
|
||||
a.parent();var b=!!a.length;b&&aa.$$addBindingClass(a);return function(a,c){var e=c.parent();b||aa.$$addBindingClass(e);aa.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function da(a,b){a=Q(a||"html");switch(a){case "svg":case "math":var c=C.document.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function ha(a,b){if("srcdoc"==b)return M.HTML;var c=wa(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=
|
||||
c&&("src"==b||"ngSrc"==b))return M.RESOURCE_URL}function ia(a,c,d,e,f){var g=ha(a,e);f=k[e]||f;var h=b(d,!0,g,f);if(h){if("multiple"===e&&"select"===wa(a))throw ga("selmulti",ya(a));c.push({priority:100,compile:function(){return{pre:function(a,c,k){c=k.$$observers||(k.$$observers=U());if(m.test(e))throw ga("nodomevents");var l=k[e];l!==d&&(h=l&&b(l,!0,g,f),d=l);h&&(k[e]=h(a),(c[e]||(c[e]=[])).$$inter=!0,(k.$$observers&&k.$$observers[e].$$scope||a).$watch(h,function(a,b){"class"===e&&a!=b?k.$updateClass(a,
|
||||
b):k.$set(e,a)}))}}}})}}function ea(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]==d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=C.document.createDocumentFragment();for(g=0;g<e;g++)a.appendChild(b[g]);F.hasData(d)&&(F.data(c,F.data(d)),F(d).off("$destroy"));F.cleanData(a.querySelectorAll("*"));for(g=1;g<e;g++)delete b[g];b[0]=c;b.length=1}function ja(a,
|
||||
b){return S(function(){return a.apply(null,arguments)},a,b)}function la(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,ya(d))}}function ka(a,c,d,e,f){function g(b,c,e){z(d.$onChanges)&&c!==e&&(Y||(a.$$postDigest(I),Y=[]),m||(m={},Y.push(h)),m[b]&&(e=m[b].previousValue),m[b]=new Fb(e,c))}function h(){d.$onChanges(m);m=void 0}var k=[],l={},m;q(e,function(e,h){var m=e.attrName,p=e.optional,v,u,x,H;switch(e.mode){case "@":p||ua.call(c,m)||(d[h]=c[m]=void 0);c.$observe(m,function(a){if(G(a)||Ga(a))g(h,a,d[h]),
|
||||
d[h]=a});c.$$observers[m].$$scope=a;v=c[m];G(v)?d[h]=b(v)(a):Ga(v)&&(d[h]=v);l[h]=new Fb(bc,d[h]);break;case "=":if(!ua.call(c,m)){if(p)break;c[m]=void 0}if(p&&!c[m])break;u=n(c[m]);H=u.literal?na:function(a,b){return a===b||a!==a&&b!==b};x=u.assign||function(){v=d[h]=u(a);throw ga("nonassign",c[m],m,f.name);};v=d[h]=u(a);p=function(b){H(b,d[h])||(H(b,v)?x(a,b=d[h]):d[h]=b);return v=b};p.$stateful=!0;p=e.collection?a.$watchCollection(c[m],p):a.$watch(n(c[m],p),null,u.literal);k.push(p);break;case "<":if(!ua.call(c,
|
||||
m)){if(p)break;c[m]=void 0}if(p&&!c[m])break;u=n(c[m]);var E=d[h]=u(a);l[h]=new Fb(bc,d[h]);p=a.$watch(u,function(a,b){if(b===a){if(b===E)return;b=E}g(h,a,b);d[h]=a},u.literal);k.push(p);break;case "&":u=c.hasOwnProperty(m)?n(c[m]):A;if(u===A&&p)break;d[h]=function(b){return u(a,b)}}});return{initialChanges:l,removeWatches:k.length&&function(){for(var a=0,b=k.length;a<b;++a)k[a]()}}}var ta=/^\w/,pa=C.document.createElement("div"),qa=u,Y;Da.prototype={$normalize:Aa,$addClass:function(a){a&&0<a.length&&
|
||||
H.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&H.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=ad(a,b);c&&c.length&&H.addClass(this.$$element,c);(c=ad(b,a))&&c.length&&H.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=Uc(this.$$element[0],a),g=bd[a],h=a;f?(this.$$element.prop(a,b),e=f):g&&(this[g]=b,h=g);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=Cc(a,"-"));f=wa(this.$$element);if("a"===f&&("href"===a||"xlinkHref"===a)||"img"===
|
||||
f&&"src"===a)this[a]=b=E(b,"src"===a);else if("img"===f&&"srcset"===a&&w(b)){for(var f="",g=W(b),k=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,k=/\s/.test(g)?k:/(,)/,g=g.split(k),k=Math.floor(g.length/2),l=0;l<k;l++)var m=2*l,f=f+E(W(g[m]),!0),f=f+(" "+W(g[m+1]));g=W(g[2*l]).split(/\s/);f+=E(W(g[0]),!0);2===g.length&&(f+=" "+W(g[1]));this[a]=b=f}!1!==d&&(null===b||y(b)?this.$$element.removeAttr(e):ta.test(e)?this.$$element.attr(e,b):P(this.$$element[0],e,b));(a=this.$$observers)&&q(a[h],function(a){try{a(b)}catch(d){c(d)}})},
|
||||
$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=U()),e=d[a]||(d[a]=[]);e.push(b);K.$evalAsync(function(){e.$$inter||!c.hasOwnProperty(a)||y(c[a])||b(c[a])});return function(){Za(e,b)}}};var ra=b.startSymbol(),sa=b.endSymbol(),xa="{{"==ra&&"}}"==sa?Xa:function(a){return a.replace(/\{\{/g,ra).replace(/}}/g,sa)},Ba=/^ngAttr[A-Z]/,Ca=/^(.+)Start$/;aa.$$addBindingInfo=p?function(a,b){var c=a.data("$binding")||[];L(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:A;aa.$$addBindingClass=
|
||||
p?function(a){x(a,"ng-binding")}:A;aa.$$addScopeInfo=p?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:A;aa.$$addScopeClass=p?function(a,b){x(a,b?"ng-isolate-scope":"ng-scope")}:A;aa.$$createComment=function(a,b){var c="";p&&(c=" "+(a||"")+": ",b&&(c+=b+" "));return C.document.createComment(c)};return aa}]}function Fb(a,b){this.previousValue=a;this.currentValue=b}function Aa(a){return db(a.replace(Yc,""))}function ad(a,b){var d="",c=a.split(/\s+/),e=b.split(/\s+/),
|
||||
f=0;a:for(;f<c.length;f++){for(var g=c[f],h=0;h<e.length;h++)if(g==e[h])continue a;d+=(0<d.length?" ":"")+g}return d}function $c(a){a=F(a);var b=a.length;if(1>=b)return a;for(;b--;)8===a[b].nodeType&&bg.call(a,b,1);return a}function Xc(a,b){if(b&&G(b))return b;if(G(a)){var d=cd.exec(a);if(d)return d[3]}}function ff(){var a={},b=!1;this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,c){Qa(b,"controller");D(b)?S(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector",
|
||||
"$window",function(d,c){function e(a,b,c,d){if(!a||!D(a.$scope))throw N("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,n;h=!0===h;k&&G(k)&&(n=k);if(G(f)){k=f.match(cd);if(!k)throw cg("ctrlfmt",f);m=k[1];n=n||k[3];f=a.hasOwnProperty(m)?a[m]:Ec(g.$scope,m,!0)||(b?Ec(c,m,!0):void 0);Pa(f,m,!0)}if(h)return h=(L(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),n&&e(g,n,l,m||f.name),S(function(){var a=d.invoke(f,l,g,m);a!==l&&(D(a)||z(a))&&(l=a,n&&e(g,n,l,m||f.name));return l},
|
||||
{instance:l,identifier:n});l=d.instantiate(f,g,m);n&&e(g,n,l,m||f.name);return l}}]}function gf(){this.$get=["$window",function(a){return F(a.document)}]}function hf(){this.$get=["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function cc(a){return D(a)?da(a)?a.toISOString():bb(a):a}function nf(){this.$get=function(){return function(a){if(!a)return"";var b=[];tc(a,function(a,c){null===a||y(a)||(L(a)?q(a,function(a){b.push(ea(c)+"="+ea(cc(a)))}):b.push(ea(c)+"="+ea(cc(a))))});
|
||||
return b.join("&")}}}function of(){this.$get=function(){return function(a){function b(a,e,f){null===a||y(a)||(L(a)?q(a,function(a,c){b(a,e+"["+(D(a)?c:"")+"]")}):D(a)&&!da(a)?tc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):d.push(ea(e)+"="+ea(cc(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function dc(a,b){if(G(a)){var d=a.replace(dg,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf(dd))||(c=(c=d.match(eg))&&fg[c[0]].test(d));c&&(a=xc(d))}}return a}function ed(a){var b=
|
||||
U(),d;G(a)?q(a.split("\n"),function(a){d=a.indexOf(":");var e=Q(W(a.substr(0,d)));a=W(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):D(a)&&q(a,function(a,d){var f=Q(d),g=W(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function fd(a){var b;return function(d){b||(b=ed(a));return d?(d=b[Q(d)],void 0===d&&(d=null),d):b}}function gd(a,b,d,c){if(z(c))return c(a,b,d);q(c,function(c){a=c(a,b,d)});return a}function mf(){var a=this.defaults={transformResponse:[dc],transformRequest:[function(a){return D(a)&&"[object File]"!==
|
||||
ma.call(a)&&"[object Blob]"!==ma.call(a)&&"[object FormData]"!==ma.call(a)?bb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ia(ec),put:ia(ec),patch:ia(ec)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer"},b=!1;this.useApplyAsync=function(a){return w(a)?(b=!!a,this):b};var d=!0;this.useLegacyPromiseExtensions=function(a){return w(a)?(d=!!a,this):d};var c=this.interceptors=[];this.$get=["$httpBackend","$$cookieReader","$cacheFactory",
|
||||
"$rootScope","$q","$injector",function(e,f,g,h,k,l){function m(b){function c(a,b){for(var d=0,e=b.length;d<e;){var f=b[d++],g=b[d++];a=a.then(f,g)}b.length=0;return a}function e(a,b){var c,d={};q(a,function(a,e){z(a)?(c=a(b),null!=c&&(d[e]=c)):d[e]=a});return d}function f(a){var b=S({},a);b.data=gd(a.data,a.headers,a.status,g.transformResponse);a=a.status;return 200<=a&&300>a?b:k.reject(b)}if(!D(b))throw N("$http")("badreq",b);if(!G(b.url))throw N("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,
|
||||
transformResponse:a.transformResponse,paramSerializer:a.paramSerializer},b);g.headers=function(b){var c=a.headers,d=S({},b.headers),f,g,h,c=S({},c.common,c[Q(b.method)]);a:for(f in c){g=Q(f);for(h in d)if(Q(h)===g)continue a;d[f]=c[f]}return e(d,ia(b))}(b);g.method=ub(g.method);g.paramSerializer=G(g.paramSerializer)?l.get(g.paramSerializer):g.paramSerializer;var h=[],m=[],p=k.when(g);q(R,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&m.push(a.response,
|
||||
a.responseError)});p=c(p,h);p=p.then(function(b){var c=b.headers,d=gd(b.data,fd(c),void 0,b.transformRequest);y(d)&&q(c,function(a,b){"content-type"===Q(b)&&delete c[b]});y(b.withCredentials)&&!y(a.withCredentials)&&(b.withCredentials=a.withCredentials);return n(b,d).then(f,f)});p=c(p,m);d?(p.success=function(a){Pa(a,"fn");p.then(function(b){a(b.data,b.status,b.headers,g)});return p},p.error=function(a){Pa(a,"fn");p.then(null,function(b){a(b.data,b.status,b.headers,g)});return p}):(p.success=hd("success"),
|
||||
p.error=hd("error"));return p}function n(c,d){function g(a){if(a){var c={};q(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function l(a,c,d,e){function f(){n(c,a,d,e)}E&&(200<=a&&300>a?E.put(P,[a,c,ed(d),e]):E.remove(P));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function n(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?M.resolve:M.reject)({data:a,status:b,headers:fd(d),config:c,statusText:e})}function t(a){n(a.data,a.status,ia(a.headers()),
|
||||
a.statusText)}function R(){var a=m.pendingRequests.indexOf(c);-1!==a&&m.pendingRequests.splice(a,1)}var M=k.defer(),H=M.promise,E,I,Da=c.headers,P=p(c.url,c.paramSerializer(c.params));m.pendingRequests.push(c);H.then(R,R);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(E=D(c.cache)?c.cache:D(a.cache)?a.cache:u);E&&(I=E.get(P),w(I)?I&&z(I.then)?I.then(t,t):L(I)?n(I[1],I[0],ia(I[2]),I[3]):n(I,200,{},"OK"):E.put(P,H));y(I)&&((I=id(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:
|
||||
void 0)&&(Da[c.xsrfHeaderName||a.xsrfHeaderName]=I),e(c.method,P,d,l,Da,c.timeout,c.withCredentials,c.responseType,g(c.eventHandlers),g(c.uploadEventHandlers)));return H}function p(a,b){0<b.length&&(a+=(-1==a.indexOf("?")?"?":"&")+b);return a}var u=g("$http");a.paramSerializer=G(a.paramSerializer)?l.get(a.paramSerializer):a.paramSerializer;var R=[];q(c,function(a){R.unshift(G(a)?l.get(a):l.invoke(a))});m.pendingRequests=[];(function(a){q(arguments,function(a){m[a]=function(b,c){return m(S({},c||{},
|
||||
{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){q(arguments,function(a){m[a]=function(b,c,d){return m(S({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");m.defaults=a;return m}]}function qf(){this.$get=function(){return function(){return new C.XMLHttpRequest}}}function pf(){this.$get=["$browser","$jsonpCallbacks","$document","$xhrFactory",function(a,b,d,c){return gg(a,c,a.defer,b,d[0])}]}function gg(a,b,d,c,e){function f(a,b,d){a=a.replace("JSON_CALLBACK",b);var f=
|
||||
e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f.async=!0;m=function(a){f.removeEventListener("load",m,!1);f.removeEventListener("error",m,!1);e.body.removeChild(f);f=null;var g=-1,u="unknown";a&&("load"!==a.type||c.wasCalled(b)||(a={type:"error"}),u=a.type,g="error"===a.type?404:200);d&&d(g,u)};f.addEventListener("load",m,!1);f.addEventListener("error",m,!1);e.body.appendChild(f);return m}return function(e,h,k,l,m,n,p,u,R,B){function r(){fa&&fa();t&&t.abort()}function J(b,c,e,
|
||||
f,g){w(M)&&d.cancel(M);fa=t=null;b(c,e,f,g);a.$$completeOutstandingRequest(A)}a.$$incOutstandingRequestCount();h=h||a.url();if("jsonp"===Q(e))var v=c.createCallback(h),fa=f(h,v,function(a,b){var d=200===a&&c.getResponse(v);J(l,a,d,"",b);c.removeCallback(v)});else{var t=b(e,h);t.open(e,h,!0);q(m,function(a,b){w(a)&&t.setRequestHeader(b,a)});t.onload=function(){var a=t.statusText||"",b="response"in t?t.response:t.responseText,c=1223===t.status?204:t.status;0===c&&(c=b?200:"file"==Y(h).protocol?404:
|
||||
0);J(l,c,b,t.getAllResponseHeaders(),a)};e=function(){J(l,-1,null,null,"")};t.onerror=e;t.onabort=e;q(R,function(a,b){t.addEventListener(b,a)});q(B,function(a,b){t.upload.addEventListener(b,a)});p&&(t.withCredentials=!0);if(u)try{t.responseType=u}catch(K){if("json"!==u)throw K;}t.send(y(k)?null:k)}if(0<n)var M=d(r,n);else n&&z(n.then)&&n.then(r)}}function kf(){var a="{{",b="}}";this.startSymbol=function(b){return b?(a=b,this):a};this.endSymbol=function(a){return a?(b=a,this):b};this.$get=["$parse",
|
||||
"$exceptionHandler","$sce",function(d,c,e){function f(a){return"\\\\\\"+a}function g(c){return c.replace(n,a).replace(p,b)}function h(a,b,c,d){var e;return e=a.$watch(function(a){e();return d(a)},b,c)}function k(f,k,p,n){function J(a){try{var b=a;a=p?e.getTrusted(p,b):e.valueOf(b);var d;if(n&&!w(a))d=a;else if(null==a)d="";else{switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=bb(a)}d=a}return d}catch(g){c(Ka.interr(f,g))}}if(!f.length||-1===f.indexOf(a)){var v;k||(k=g(f),
|
||||
v=ha(k),v.exp=f,v.expressions=[],v.$$watchDelegate=h);return v}n=!!n;var q,t,K=0,M=[],H=[];v=f.length;for(var E=[],I=[];K<v;)if(-1!=(q=f.indexOf(a,K))&&-1!=(t=f.indexOf(b,q+l)))K!==q&&E.push(g(f.substring(K,q))),K=f.substring(q+l,t),M.push(K),H.push(d(K,J)),K=t+m,I.push(E.length),E.push("");else{K!==v&&E.push(g(f.substring(K)));break}p&&1<E.length&&Ka.throwNoconcat(f);if(!k||M.length){var Da=function(a){for(var b=0,c=M.length;b<c;b++){if(n&&y(a[b]))return;E[I[b]]=a[b]}return E.join("")};return S(function(a){var b=
|
||||
0,d=M.length,e=Array(d);try{for(;b<d;b++)e[b]=H[b](a);return Da(e)}catch(g){c(Ka.interr(f,g))}},{exp:f,expressions:M,$$watchDelegate:function(a,b){var c;return a.$watchGroup(H,function(d,e){var f=Da(d);z(b)&&b.call(this,f,d!==e?c:f,a);c=f})}})}}var l=a.length,m=b.length,n=new RegExp(a.replace(/./g,f),"g"),p=new RegExp(b.replace(/./g,f),"g");k.startSymbol=function(){return a};k.endSymbol=function(){return b};return k}]}function lf(){this.$get=["$rootScope","$window","$q","$$q","$browser",function(a,
|
||||
b,d,c,e){function f(f,k,l,m){function n(){p?f.apply(null,u):f(r)}var p=4<arguments.length,u=p?va.call(arguments,4):[],R=b.setInterval,q=b.clearInterval,r=0,J=w(m)&&!m,v=(J?c:d).defer(),fa=v.promise;l=w(l)?l:0;fa.$$intervalId=R(function(){J?e.defer(n):a.$evalAsync(n);v.notify(r++);0<l&&r>=l&&(v.resolve(r),q(fa.$$intervalId),delete g[fa.$$intervalId]);J||a.$apply()},k);g[fa.$$intervalId]=v;return fa}var g={};f.cancel=function(a){return a&&a.$$intervalId in g?(g[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),
|
||||
delete g[a.$$intervalId],!0):!1};return f}]}function fc(a){a=a.split("/");for(var b=a.length;b--;)a[b]=qb(a[b]);return a.join("/")}function jd(a,b){var d=Y(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=Z(d.port)||hg[d.protocol]||null}function kd(a,b){var d="/"!==a.charAt(0);d&&(a="/"+a);var c=Y(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=Ac(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!=b.$$path.charAt(0)&&(b.$$path=
|
||||
"/"+b.$$path)}function ka(a,b){if(0===b.lastIndexOf(a,0))return b.substr(a.length)}function Ja(a){var b=a.indexOf("#");return-1==b?a:a.substr(0,b)}function jb(a){return a.replace(/(#.+)|#$/,"$1")}function gc(a,b,d){this.$$html5=!0;d=d||"";jd(a,this);this.$$parse=function(a){var d=ka(b,a);if(!G(d))throw Gb("ipthprfx",a,b);kd(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Tb(this.$$search),d=this.$$hash?"#"+qb(this.$$hash):"";this.$$url=fc(this.$$path)+(a?"?"+
|
||||
a:"")+d;this.$$absUrl=b+this.$$url.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;w(f=ka(a,c))?(g=f,g=w(f=ka(d,f))?b+(ka("/",f)||f):a+g):w(f=ka(b,c))?g=b+f:b==c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function hc(a,b,d){jd(a,this);this.$$parse=function(c){var e=ka(a,c)||ka(b,c),f;y(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",y(e)&&(a=c,this.replace())):(f=ka(d,e),y(f)&&(f=e));kd(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;0===f.lastIndexOf(e,
|
||||
0)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Tb(this.$$search),e=this.$$hash?"#"+qb(this.$$hash):"";this.$$url=fc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url?d+this.$$url:"")};this.$$parseLinkUrl=function(b,d){return Ja(a)==Ja(b)?(this.$$parse(b),!0):!1}}function ld(a,b,d){this.$$html5=!0;hc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a==Ja(c)?
|
||||
f=c:(g=ka(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Tb(this.$$search),e=this.$$hash?"#"+qb(this.$$hash):"";this.$$url=fc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url}}function Hb(a){return function(){return this[a]}}function md(a,b){return function(d){if(y(d))return this[a];this[a]=b(d);this.$$compose();return this}}function sf(){var a="",b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return w(b)?(a=b,this):
|
||||
a};this.html5Mode=function(a){return Ga(a)?(b.enabled=a,this):D(a)?(Ga(a.enabled)&&(b.enabled=a.enabled),Ga(a.requireBase)&&(b.requireBase=a.requireBase),Ga(a.rewriteLinks)&&(b.rewriteLinks=a.rewriteLinks),this):b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state=f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,
|
||||
b)}var l,m;m=c.baseHref();var n=c.url(),p;if(b.enabled){if(!m&&b.requireBase)throw Gb("nobase");p=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(m||"/");m=e.history?gc:ld}else p=Ja(n),m=hc;var u=p.substr(0,Ja(p).lastIndexOf("/")+1);l=new m(p,u,"#"+a);l.$$parseLinkUrl(n,n);l.$$state=c.state();var R=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(b.rewriteLinks&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!=a.which&&2!=a.button){for(var e=F(a.target);"a"!==wa(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return;
|
||||
var h=e.prop("href"),k=e.attr("href")||e.attr("xlink:href");D(h)&&"[object SVGAnimatedString]"===h.toString()&&(h=Y(h.animVal).href);R.test(h)||!h||e.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(h,k)||(a.preventDefault(),l.absUrl()!=c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]=!0))}});jb(l.absUrl())!=jb(n)&&c.url(l.absUrl(),!0);var q=!0;c.onUrlChange(function(a,b){y(ka(u,a))?g.location.href=a:(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=jb(a);l.$$parse(a);l.$$state=
|
||||
b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(q=!1,k(c,e)))}),d.$$phase||d.$digest())});d.$watch(function(){var a=jb(c.url()),b=jb(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(q||m)q=!1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:l.$$state),
|
||||
k(a,f)))});l.$$replace=!1});return l}]}function tf(){var a=!0,b=this;this.debugEnabled=function(b){return w(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||A;a=!1;try{a=!!e.apply}catch(k){}return a?function(){var a=[];q(arguments,function(b){a.push(c(b))});
|
||||
return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Sa(a,b){if("__defineGetter__"===a||"__defineSetter__"===a||"__lookupGetter__"===a||"__lookupSetter__"===a||"__proto__"===a)throw X("isecfld",b);return a}function ig(a){return a+""}function ra(a,b){if(a){if(a.constructor===a)throw X("isecfn",b);if(a.window===a)throw X("isecwindow",b);if(a.children&&
|
||||
(a.nodeName||a.prop&&a.attr&&a.find))throw X("isecdom",b);if(a===Object)throw X("isecobj",b);}return a}function nd(a,b){if(a){if(a.constructor===a)throw X("isecfn",b);if(a===jg||a===kg||a===lg)throw X("isecff",b);}}function Ib(a,b){if(a&&(a===(0).constructor||a===(!1).constructor||a==="".constructor||a==={}.constructor||a===[].constructor||a===Function.constructor))throw X("isecaf",b);}function mg(a,b){return"undefined"!==typeof a?a:b}function od(a,b){return"undefined"===typeof a?b:"undefined"===
|
||||
typeof b?a:a+b}function V(a,b){var d,c;switch(a.type){case s.Program:d=!0;q(a.body,function(a){V(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:V(a.argument,b);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:V(a.left,b);V(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:V(a.left,b);V(a.right,
|
||||
b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:V(a.test,b);V(a.alternate,b);V(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant=!1;a.toWatch=[a];break;case s.MemberExpression:V(a.object,b);a.computed&&V(a.property,b);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=a.filter?!b(a.callee.name).$stateful:
|
||||
!1;c=[];q(a.arguments,function(a){V(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=a.filter&&!b(a.callee.name).$stateful?c:[a];break;case s.AssignmentExpression:V(a.left,b);V(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];q(a.elements,function(a){V(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=c;break;case s.ObjectExpression:d=!0;c=[];q(a.properties,function(a){V(a.value,
|
||||
b);d=d&&a.value.constant&&!a.computed;a.value.constant||c.push.apply(c,a.value.toWatch)});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1;a.toWatch=[];break;case s.LocalsExpression:a.constant=!1,a.toWatch=[]}}function pd(a){if(1==a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function qd(a){return a.type===s.Identifier||a.type===s.MemberExpression}function rd(a){if(1===a.body.length&&qd(a.body[0].expression))return{type:s.AssignmentExpression,
|
||||
left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function sd(a){return 0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression)}function td(a,b){this.astBuilder=a;this.$filter=b}function ud(a,b){this.astBuilder=a;this.$filter=b}function Jb(a){return"constructor"==a}function ic(a){return z(a.valueOf)?a.valueOf():ng.call(a)}function uf(){var a=U(),b=U(),d={"true":!0,
|
||||
"false":!1,"null":null,undefined:void 0},c,e;this.addLiteral=function(a,b){d[a]=b};this.setIdentifierFns=function(a,b){c=a;e=b;return this};this.$get=["$filter",function(f){function g(c,d,e){var g,k,H;e=e||J;switch(typeof c){case "string":H=c=c.trim();var E=e?b:a;g=E[H];if(!g){":"===c.charAt(0)&&":"===c.charAt(1)&&(k=!0,c=c.substring(2));g=e?r:B;var q=new jc(g);g=(new kc(q,f,g)).parse(c);g.constant?g.$$watchDelegate=p:k?g.$$watchDelegate=g.literal?n:m:g.inputs&&(g.$$watchDelegate=l);e&&(g=h(g));E[H]=
|
||||
g}return u(g,d);case "function":return u(c,d);default:return u(A,d)}}function h(a){function b(c,d,e,f){var g=J;J=!0;try{return a(c,d,e,f)}finally{J=g}}if(!a)return a;b.$$watchDelegate=a.$$watchDelegate;b.assign=h(a.assign);b.constant=a.constant;b.literal=a.literal;for(var c=0;a.inputs&&c<a.inputs.length;++c)a.inputs[c]=h(a.inputs[c]);b.inputs=a.inputs;return b}function k(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=ic(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function l(a,b,c,d,
|
||||
e){var f=d.inputs,g;if(1===f.length){var h=k,f=f[0];return a.$watch(function(a){var b=f(a);k(b,h)||(g=d(a,void 0,void 0,[b]),h=b&&ic(b));return g},b,c,e)}for(var l=[],m=[],p=0,n=f.length;p<n;p++)l[p]=k,m[p]=null;return a.$watch(function(a){for(var b=!1,c=0,e=f.length;c<e;c++){var h=f[c](a);if(b||(b=!k(h,l[c])))m[c]=h,l[c]=h&&ic(h)}b&&(g=d(a,void 0,void 0,m));return g},b,c,e)}function m(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;z(b)&&b.apply(this,arguments);w(a)&&
|
||||
d.$$postDigest(function(){w(f)&&e()})},c)}function n(a,b,c,d){function e(a){var b=!0;q(a,function(a){w(a)||(b=!1)});return b}var f,g;return f=a.$watch(function(a){return d(a)},function(a,c,d){g=a;z(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(g)&&f()})},c)}function p(a,b,c,d){var e;return e=a.$watch(function(a){e();return d(a)},b,c)}function u(a,b){if(!b)return a;var c=a.$$watchDelegate,d=!1,c=c!==n&&c!==m?function(c,e,f,g){f=d&&g?g[0]:a(c,e,f,g);return b(f,c,e)}:function(c,d,e,f){e=a(c,
|
||||
d,e,f);c=b(e,c,d);return w(e)?c:e};a.$$watchDelegate&&a.$$watchDelegate!==l?c.$$watchDelegate=a.$$watchDelegate:b.$stateful||(c.$$watchDelegate=l,d=!a.inputs,c.inputs=a.inputs?a.inputs:[a]);return c}var R=Ba().noUnsafeEval,B={csp:R,expensiveChecks:!1,literals:pa(d),isIdentifierStart:z(c)&&c,isIdentifierContinue:z(e)&&e},r={csp:R,expensiveChecks:!0,literals:pa(d),isIdentifierStart:z(c)&&c,isIdentifierContinue:z(e)&&e},J=!1;g.$$runningExpensiveChecks=function(){return J};return g}]}function wf(){this.$get=
|
||||
["$rootScope","$exceptionHandler",function(a,b){return vd(function(b){a.$evalAsync(b)},b)}]}function xf(){this.$get=["$browser","$exceptionHandler",function(a,b){return vd(function(b){a.defer(b)},b)}]}function vd(a,b){function d(){this.$$state={status:0}}function c(a,b){return function(c){b.call(a,c)}}function e(c){!c.processScheduled&&c.pending&&(c.processScheduled=!0,a(function(){var a,d,e;e=c.pending;c.processScheduled=!1;c.pending=void 0;for(var f=0,g=e.length;f<g;++f){d=e[f][0];a=e[f][c.status];
|
||||
try{z(a)?d.resolve(a(c.value)):1===c.status?d.resolve(c.value):d.reject(c.value)}catch(h){d.reject(h),b(h)}}}))}function f(){this.promise=new d}var g=N("$q",TypeError),h=function(){var a=new f;a.resolve=c(a,a.resolve);a.reject=c(a,a.reject);a.notify=c(a,a.notify);return a};S(d.prototype,{then:function(a,b,c){if(y(a)&&y(b)&&y(c))return this;var d=new f;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&e(this.$$state);return d.promise},"catch":function(a){return this.then(null,
|
||||
a)},"finally":function(a,b){return this.then(function(b){return l(b,!0,a)},function(b){return l(b,!1,a)},b)}});S(f.prototype,{resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(g("qcycle",a)):this.$$resolve(a))},$$resolve:function(a){function d(a){k||(k=!0,h.$$resolve(a))}function f(a){k||(k=!0,h.$$reject(a))}var g,h=this,k=!1;try{if(D(a)||z(a))g=a&&a.then;z(g)?(this.promise.$$state.status=-1,g.call(a,d,f,c(this,this.notify))):(this.promise.$$state.value=a,this.promise.$$state.status=
|
||||
1,e(this.promise.$$state))}catch(l){f(l),b(l)}},reject:function(a){this.promise.$$state.status||this.$$reject(a)},$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;e(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&d&&d.length&&a(function(){for(var a,e,f=0,g=d.length;f<g;f++){e=d[f][0];a=d[f][3];try{e.notify(z(a)?a(c):c)}catch(h){b(h)}}})}});var k=function(a,b){var c=new f;b?c.resolve(a):c.reject(a);return c.promise},
|
||||
l=function(a,b,c){var d=null;try{z(c)&&(d=c())}catch(e){return k(e,!1)}return d&&z(d.then)?d.then(function(){return k(a,b)},function(a){return k(a,!1)}):k(a,b)},m=function(a,b,c,d){var e=new f;e.resolve(a);return e.promise.then(b,c,d)},n=function(a){if(!z(a))throw g("norslvr",a);var b=new f;a(function(a){b.resolve(a)},function(a){b.reject(a)});return b.promise};n.prototype=d.prototype;n.defer=h;n.reject=function(a){var b=new f;b.reject(a);return b.promise};n.when=m;n.resolve=m;n.all=function(a){var b=
|
||||
new f,c=0,d=L(a)?[]:{};q(a,function(a,e){c++;m(a).then(function(a){d.hasOwnProperty(e)||(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};n.race=function(a){var b=h();q(a,function(a){m(a).then(b.resolve,b.reject)});return b.promise};return n}function Gf(){this.$get=["$window","$timeout",function(a,b){var d=a.requestAnimationFrame||a.webkitRequestAnimationFrame,c=a.cancelAnimationFrame||a.webkitCancelAnimationFrame||a.webkitCancelRequestAnimationFrame,
|
||||
e=!!d,f=e?function(a){var b=d(a);return function(){c(b)}}:function(a){var c=b(a,16.66,!1);return function(){b.cancel(c)}};f.supported=e;return f}]}function vf(){function a(a){function b(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$id=++pb;this.$$ChildScope=null}b.prototype=a;return b}var b=10,d=N("$rootScope"),c=null,e=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=
|
||||
["$exceptionHandler","$parse","$browser",function(f,g,h){function k(a){a.currentScope.$$destroyed=!0}function l(a){9===Ea&&(a.$$childHead&&l(a.$$childHead),a.$$nextSibling&&l(a.$$nextSibling));a.$parent=a.$$nextSibling=a.$$prevSibling=a.$$childHead=a.$$childTail=a.$root=a.$$watchers=null}function m(){this.$id=++pb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=this;this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount=
|
||||
{};this.$$watchersCount=0;this.$$isolateBindings=null}function n(a){if(J.$$phase)throw d("inprog",J.$$phase);J.$$phase=a}function p(a,b){do a.$$watchersCount+=b;while(a=a.$parent)}function u(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function s(){}function B(){for(;t.length;)try{t.shift()()}catch(a){f(a)}e=null}function r(){null===e&&(e=h.defer(function(){J.$apply(B)}))}m.prototype={constructor:m,$new:function(b,c){var d;c=c||this;b?
|
||||
(d=new m,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=a(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(b||c!=this)&&d.$on("$destroy",k);return d},$watch:function(a,b,d,e){var f=g(a);if(f.$$watchDelegate)return f.$$watchDelegate(this,b,d,f,a);var h=this,k=h.$$watchers,l={fn:b,last:s,get:f,exp:e||a,eq:!!d};c=null;z(b)||(l.fn=A);k||(k=h.$$watchers=[]);k.unshift(l);p(this,
|
||||
1);return function(){0<=Za(k,l)&&p(h,-1);c=null}},$watchGroup:function(a,b){function c(){h=!1;k?(k=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,k=!0;if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});q(a,function(a,b){var k=g.$watch(a,function(a,f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},
|
||||
$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!y(e)){if(D(e))if(ta(e))for(f!==n&&(f=n,u=f.length=0,l++),a=e.length,u!==a&&(l++,f.length=u=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==p&&(f=p={},u=0,l++);a=0;for(b in e)ua.call(e,b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(u++,f[b]=g,l++));if(u>a)for(b in l++,f)ua.call(e,b)||(u--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1<b.length,l=0,m=
|
||||
g(a,c),n=[],p={},r=!0,u=0;return this.$watch(m,function(){r?(r=!1,b(e,e,d)):b(e,h,d);if(k)if(D(e))if(ta(e)){h=Array(e.length);for(var a=0;a<e.length;a++)h[a]=e[a]}else for(a in h={},e)ua.call(e,a)&&(h[a]=e[a]);else h=e})},$digest:function(){var a,g,k,l,m,p,u,r,q=b,t,y=[],A,C;n("$digest");h.$$checkUrlChange();this===J&&null!==e&&(h.defer.cancel(e),B());c=null;do{r=!1;t=this;for(p=0;p<v.length;p++){try{C=v[p],C.scope.$eval(C.expression,C.locals)}catch(F){f(F)}c=null}v.length=0;a:do{if(p=t.$$watchers)for(u=
|
||||
p.length;u--;)try{if(a=p[u])if(m=a.get,(g=m(t))!==(k=a.last)&&!(a.eq?na(g,k):"number"===typeof g&&"number"===typeof k&&isNaN(g)&&isNaN(k)))r=!0,c=a,a.last=a.eq?pa(g,null):g,l=a.fn,l(g,k===s?g:k,t),5>q&&(A=4-q,y[A]||(y[A]=[]),y[A].push({msg:z(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:k}));else if(a===c){r=!1;break a}}catch(G){f(G)}if(!(p=t.$$watchersCount&&t.$$childHead||t!==this&&t.$$nextSibling))for(;t!==this&&!(p=t.$$nextSibling);)t=t.$parent}while(t=p);if((r||v.length)&&
|
||||
!q--)throw J.$$phase=null,d("infdig",b,y);}while(r||v.length);for(J.$$phase=null;K<w.length;)try{w[K++]()}catch(D){f(D)}w.length=K=0},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===J&&h.$$applicationDestroyed();p(this,-this.$$watchersCount);for(var b in this.$$listenerCount)u(this,this.$$listenerCount[b],b);a&&a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&
|
||||
(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=A;this.$on=this.$watch=this.$watchGroup=function(){return A};this.$$listeners={};this.$$nextSibling=null;l(this)}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a,b){J.$$phase||v.length||h.defer(function(){v.length&&J.$digest()});v.push({scope:this,expression:g(a),locals:b})},$$postDigest:function(a){w.push(a)},
|
||||
$apply:function(a){try{n("$apply");try{return this.$eval(a)}finally{J.$$phase=null}}catch(b){f(b)}finally{try{J.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&t.push(b);a=g(a);r()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,u(e,1,a))}},$emit:function(a,
|
||||
b){var c=[],d,e=this,g=!1,h={name:a,targetScope:e,stopPropagation:function(){g=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=$a([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;l<m;l++)if(d[l])try{d[l].apply(null,k)}catch(n){f(n)}else d.splice(l,1),l--,m--;if(g)return h.currentScope=null,h;e=e.$parent}while(e);h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=
|
||||
!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var g=$a([e],arguments,1),h,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,g)}catch(l){f(l)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var J=new m,v=J.$$asyncQueue=[],w=J.$$postDigestQueue=[],t=J.$$applyAsyncQueue=[],K=0;return J}]}function ne(){var a=
|
||||
/^\s*(https?|ftp|mailto|tel|file):/,b=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(b){return w(b)?(a=b,this):a};this.imgSrcSanitizationWhitelist=function(a){return w(a)?(b=a,this):b};this.$get=function(){return function(d,c){var e=c?b:a,f;f=Y(d).href;return""===f||f.match(e)?d:"unsafe:"+f}}}function og(a){if("self"===a)return a;if(G(a)){if(-1<a.indexOf("***"))throw sa("iwcard",a);a=wd(a).replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+
|
||||
a+"$")}if(Wa(a))return new RegExp("^"+a.source+"$");throw sa("imatcher");}function xd(a){var b=[];w(a)&&q(a,function(a){b.push(og(a))});return b}function zf(){this.SCE_CONTEXTS=la;var a=["self"],b=[];this.resourceUrlWhitelist=function(b){arguments.length&&(a=xd(b));return a};this.resourceUrlBlacklist=function(a){arguments.length&&(b=xd(a));return b};this.$get=["$injector",function(d){function c(a,b){return"self"===a?id(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=
|
||||
function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var f=function(a){throw sa("unsafe");};d.has("$sanitize")&&(f=d.get("$sanitize"));var g=e(),h={};h[la.HTML]=e(g);h[la.CSS]=e(g);h[la.URL]=e(g);h[la.JS]=e(g);h[la.RESOURCE_URL]=e(h[la.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw sa("icontext",a,b);if(null===b||y(b)||
|
||||
""===b)return b;if("string"!==typeof b)throw sa("itype",a);return new c(b)},getTrusted:function(d,e){if(null===e||y(e)||""===e)return e;var g=h.hasOwnProperty(d)?h[d]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue();if(d===la.RESOURCE_URL){var g=Y(e.toString()),n,p,u=!1;n=0;for(p=a.length;n<p;n++)if(c(a[n],g)){u=!0;break}if(u)for(n=0,p=b.length;n<p;n++)if(c(b[n],g)){u=!1;break}if(u)return e;throw sa("insecurl",e.toString());}if(d===la.HTML)return f(e);throw sa("unsafe");},valueOf:function(a){return a instanceof
|
||||
g?a.$$unwrapTrustedValue():a}}}]}function yf(){var a=!0;this.enabled=function(b){arguments.length&&(a=!!b);return a};this.$get=["$parse","$sceDelegate",function(b,d){if(a&&8>Ea)throw sa("iequirks");var c=ia(la);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Xa);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,
|
||||
f=c.getTrusted,g=c.trustAs;q(la,function(a,b){var d=Q(b);c[db("parse_as_"+d)]=function(b){return e(a,b)};c[db("get_trusted_"+d)]=function(b){return f(a,b)};c[db("trust_as_"+d)]=function(b){return g(a,b)}});return c}]}function Af(){this.$get=["$window","$document",function(a,b){var d={},c=!(a.chrome&&a.chrome.app&&a.chrome.app.runtime)&&a.history&&a.history.pushState,e=Z((/android (\d+)/.exec(Q((a.navigator||{}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},h,k=/^(Moz|webkit|ms)(?=[A-Z])/,
|
||||
l=g.body&&g.body.style,m=!1,n=!1;if(l){for(var p in l)if(m=k.exec(p)){h=m[0];h=h[0].toUpperCase()+h.substr(1);break}h||(h="WebkitOpacity"in l&&"webkit");m=!!("transition"in l||h+"Transition"in l);n=!!("animation"in l||h+"Animation"in l);!e||m&&n||(m=G(l.webkitTransition),n=G(l.webkitAnimation))}return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&11>=Ea)return!1;if(y(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ba(),vendorPrefix:h,transitions:m,animations:n,android:e}}]}
|
||||
function Cf(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$templateCache","$http","$q","$sce",function(b,d,c,e){function f(g,h){f.totalPendingRequests++;if(!G(g)||y(b.get(g)))g=e.getTrustedResourceUrl(g);var k=d.defaults&&d.defaults.transformResponse;L(k)?k=k.filter(function(a){return a!==dc}):k===dc&&(k=null);return d.get(g,S({cache:b,transformResponse:k},a))["finally"](function(){f.totalPendingRequests--}).then(function(a){b.put(g,a.data);return a.data},function(a){if(!h)throw pg("tpload",
|
||||
g,a.status,a.statusText);return c.reject(a)})}f.totalPendingRequests=0;return f}]}function Df(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];q(a,function(a){var c=ca.element(a).data("$binding");c&&q(c,function(c){d?(new RegExp("(^|\\s)"+wd(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!=c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var k=
|
||||
a.querySelectorAll("["+g[h]+"model"+(d?"=":"*=")+'"'+b+'"]');if(k.length)return k}},getLocation:function(){return d.url()},setLocation:function(b){b!==d.url()&&(d.url(b),a.$digest())},whenStable:function(a){b.notifyWhenNoOutstandingRequests(a)}}}]}function Ef(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(a,b,d,c,e){function f(f,k,l){z(f)||(l=k,k=f,f=A);var m=va.call(arguments,3),n=w(l)&&!l,p=(n?c:d).defer(),u=p.promise,q;q=b.defer(function(){try{p.resolve(f.apply(null,
|
||||
m))}catch(b){p.reject(b),e(b)}finally{delete g[u.$$timeoutId]}n||a.$apply()},k);u.$$timeoutId=q;g[q]=p;return u}var g={};f.cancel=function(a){return a&&a.$$timeoutId in g?(g[a.$$timeoutId].reject("canceled"),delete g[a.$$timeoutId],b.defer.cancel(a.$$timeoutId)):!1};return f}]}function Y(a){Ea&&($.setAttribute("href",a),a=$.href);$.setAttribute("href",a);return{href:$.href,protocol:$.protocol?$.protocol.replace(/:$/,""):"",host:$.host,search:$.search?$.search.replace(/^\?/,""):"",hash:$.hash?$.hash.replace(/^#/,
|
||||
""):"",hostname:$.hostname,port:$.port,pathname:"/"===$.pathname.charAt(0)?$.pathname:"/"+$.pathname}}function id(a){a=G(a)?Y(a):a;return a.protocol===yd.protocol&&a.host===yd.host}function Ff(){this.$get=ha(C)}function zd(a){function b(a){try{return decodeURIComponent(a)}catch(b){return a}}var d=a[0]||{},c={},e="";return function(){var a,g,h,k,l;a=d.cookie||"";if(a!==e)for(e=a,a=e.split("; "),c={},h=0;h<a.length;h++)g=a[h],k=g.indexOf("="),0<k&&(l=b(g.substring(0,k)),y(c[l])&&(c[l]=b(g.substring(k+
|
||||
1))));return c}}function Jf(){this.$get=zd}function Mc(a){function b(d,c){if(D(d)){var e={};q(d,function(a,c){e[c]=b(c,a)});return e}return a.factory(d+"Filter",c)}this.register=b;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];b("currency",Ad);b("date",Bd);b("filter",qg);b("json",rg);b("limitTo",sg);b("lowercase",tg);b("number",Cd);b("orderBy",Dd);b("uppercase",ug)}function qg(){return function(a,b,d,c){if(!ta(a)){if(null==a)return a;throw N("filter")("notarray",
|
||||
a);}c=c||"$";var e;switch(lc(b)){case "function":break;case "boolean":case "null":case "number":case "string":e=!0;case "object":b=vg(b,d,c,e);break;default:return a}return Array.prototype.filter.call(a,b)}}function vg(a,b,d,c){var e=D(a)&&d in a;!0===b?b=na:z(b)||(b=function(a,b){if(y(a))return!1;if(null===a||null===b)return a===b;if(D(b)||D(a)&&!vc(a))return!1;a=Q(""+a);b=Q(""+b);return-1!==a.indexOf(b)});return function(f){return e&&!D(f)?La(f,a[d],b,d,!1):La(f,a,b,d,c)}}function La(a,b,d,c,e,
|
||||
f){var g=lc(a),h=lc(b);if("string"===h&&"!"===b.charAt(0))return!La(a,b.substring(1),d,c,e);if(L(a))return a.some(function(a){return La(a,b,d,c,e)});switch(g){case "object":var k;if(e){for(k in a)if("$"!==k.charAt(0)&&La(a[k],b,d,c,!0))return!0;return f?!1:La(a,b,d,c,!1)}if("object"===h){for(k in b)if(f=b[k],!z(f)&&!y(f)&&(g=k===c,!La(g?a:a[k],f,d,c,g,g)))return!1;return!0}return d(a,b);case "function":return!1;default:return d(a,b)}}function lc(a){return null===a?"null":typeof a}function Ad(a){var b=
|
||||
a.NUMBER_FORMATS;return function(a,c,e){y(c)&&(c=b.CURRENCY_SYM);y(e)&&(e=b.PATTERNS[1].maxFrac);return null==a?a:Ed(a,b.PATTERNS[1],b.GROUP_SEP,b.DECIMAL_SEP,e).replace(/\u00A4/g,c)}}function Cd(a){var b=a.NUMBER_FORMATS;return function(a,c){return null==a?a:Ed(a,b.PATTERNS[0],b.GROUP_SEP,b.DECIMAL_SEP,c)}}function wg(a){var b=0,d,c,e,f,g;-1<(c=a.indexOf(Fd))&&(a=a.replace(Fd,""));0<(e=a.search(/e/i))?(0>c&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)==mc;e++);
|
||||
if(e==(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)==mc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Gd&&(d=d.splice(0,Gd-1),b=c-1,c=1);return{d:d,e:b,i:c}}function xg(a,b,d,c){var e=a.d,f=e.length-a.i;b=y(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0<d){e.splice(Math.max(a.i,d));for(var g=d;g<e.length;g++)e[g]=0}else for(f=Math.max(0,f),a.i=1,e.length=Math.max(1,d=b+1),e[0]=0,g=1;g<d;g++)e[g]=0;if(5<=c)if(0>d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;
|
||||
for(;f<Math.max(0,b);f++)e.push(0);if(b=e.reduceRight(function(a,b,c,d){b+=a;d[c]=b%10;return Math.floor(b/10)},0))e.unshift(b),a.i++}function Ed(a,b,d,c,e){if(!G(a)&&!T(a)||isNaN(a))return"";var f=!isFinite(a),g=!1,h=Math.abs(a)+"",k="";if(f)k="\u221e";else{g=wg(h);xg(g,e,b.minFrac,b.maxFrac);k=g.d;h=g.i;e=g.e;f=[];for(g=k.reduce(function(a,b){return a&&!b},!0);0>h;)k.unshift(0),h++;0<h?f=k.splice(h,k.length):(f=k,k=[0]);h=[];for(k.length>=b.lgSize&&h.unshift(k.splice(-b.lgSize,k.length).join(""));k.length>
|
||||
b.gSize;)h.unshift(k.splice(-b.gSize,k.length).join(""));k.length&&h.unshift(k.join(""));k=h.join(d);f.length&&(k+=c+f.join(""));e&&(k+="e+"+e)}return 0>a&&!g?b.negPre+k+b.negSuf:b.posPre+k+b.posSuf}function Kb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length<b;)a=mc+a;d&&(a=a.substr(a.length-b));return e+a}function ba(a,b,d,c,e){d=d||0;return function(f){f=f["get"+a]();if(0<d||f>-d)f+=d;0===f&&-12==d&&(f=12);return Kb(f,b,c,e)}}function kb(a,b,d){return function(c,e){var f=
|
||||
c["get"+a](),g=ub((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Hd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Id(a){return function(b){var d=Hd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Kb(b,a)}}function nc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Bd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,
|
||||
k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Z(b[9]+b[10]),g=Z(b[9]+b[11]));h.call(a,Z(b[1]),Z(b[2])-1,Z(b[3]));f=Z(b[4]||0)-f;g=Z(b[5]||0)-g;h=Z(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",h=[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;G(c)&&(c=yg.test(c)?Z(c):b(c));T(c)&&(c=new Date(c));if(!da(c)||!isFinite(c.getTime()))return c;
|
||||
for(;d;)(l=zg.exec(d))?(h=$a(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=yc(f,m),c=Sb(c,f,!0));q(h,function(b){k=Ag[b];g+=k?k(c,a.DATETIME_FORMATS,m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function rg(){return function(a,b){y(b)&&(b=2);return bb(a,b)}}function sg(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):Z(b);if(isNaN(b))return a;T(a)&&(a=a.toString());if(!ta(a))return a;d=!d||isNaN(d)?0:Z(d);d=0>d?Math.max(0,a.length+
|
||||
d):d;return 0<=b?oc(a,d,d+b):0===d?oc(a,b,a.length):oc(a,Math.max(0,d+b),d)}}function oc(a,b,d){return G(a)?a.slice(b,d):va.call(a,b,d)}function Dd(a){function b(b){return b.map(function(b){var c=1,d=Xa;if(z(b))d=b;else if(G(b)){if("+"==b.charAt(0)||"-"==b.charAt(0))c="-"==b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}
|
||||
function c(a,b){var c=0,d=a.type,k=b.type;if(d===k){var k=a.value,l=b.value;"string"===d?(k=k.toLowerCase(),l=l.toLowerCase()):"object"===d&&(D(k)&&(k=a.index),D(l)&&(l=b.index));k!==l&&(c=k<l?-1:1)}else c=d<k?-1:1;return c}return function(a,f,g,h){if(null==a)return a;if(!ta(a))throw N("orderBy")("notarray",a);L(f)||(f=[f]);0===f.length&&(f=["+"]);var k=b(f),l=g?-1:1,m=z(h)?h:c;a=Array.prototype.map.call(a,function(a,b){return{value:a,tieBreaker:{value:b,type:"number",index:b},predicateValues:k.map(function(c){var e=
|
||||
c.get(a);c=typeof e;if(null===e)c="string",e="null";else if("object"===c)a:{if(z(e.valueOf)&&(e=e.valueOf(),d(e)))break a;vc(e)&&(e=e.toString(),d(e))}return{value:e,type:c,index:b}})}});a.sort(function(a,b){for(var c=0,d=k.length;c<d;c++){var e=m(a.predicateValues[c],b.predicateValues[c]);if(e)return e*k[c].descending*l}return m(a.tieBreaker,b.tieBreaker)*l});return a=a.map(function(a){return a.value})}}function Ta(a){z(a)&&(a={link:a});a.restrict=a.restrict||"AC";return ha(a)}function Jd(a,b,d,
|
||||
c,e){var f=this,g=[];f.$error={};f.$$success={};f.$pending=void 0;f.$name=e(b.name||b.ngForm||"")(d);f.$dirty=!1;f.$pristine=!0;f.$valid=!0;f.$invalid=!1;f.$submitted=!1;f.$$parentForm=Lb;f.$rollbackViewValue=function(){q(g,function(a){a.$rollbackViewValue()})};f.$commitViewValue=function(){q(g,function(a){a.$commitViewValue()})};f.$addControl=function(a){Qa(a.$name,"input");g.push(a);a.$name&&(f[a.$name]=a);a.$$parentForm=f};f.$$renameControl=function(a,b){var c=a.$name;f[c]===a&&delete f[c];f[b]=
|
||||
a;a.$name=b};f.$removeControl=function(a){a.$name&&f[a.$name]===a&&delete f[a.$name];q(f.$pending,function(b,c){f.$setValidity(c,null,a)});q(f.$error,function(b,c){f.$setValidity(c,null,a)});q(f.$$success,function(b,c){f.$setValidity(c,null,a)});Za(g,a);a.$$parentForm=Lb};Kd({ctrl:this,$element:a,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){var d=a[b];d&&(Za(d,c),0===d.length&&delete a[b])},$animate:c});f.$setDirty=function(){c.removeClass(a,Ua);c.addClass(a,
|
||||
Mb);f.$dirty=!0;f.$pristine=!1;f.$$parentForm.$setDirty()};f.$setPristine=function(){c.setClass(a,Ua,Mb+" ng-submitted");f.$dirty=!1;f.$pristine=!0;f.$submitted=!1;q(g,function(a){a.$setPristine()})};f.$setUntouched=function(){q(g,function(a){a.$setUntouched()})};f.$setSubmitted=function(){c.addClass(a,"ng-submitted");f.$submitted=!0;f.$$parentForm.$setSubmitted()}}function pc(a){a.$formatters.push(function(b){return a.$isEmpty(b)?b:b.toString()})}function lb(a,b,d,c,e,f){var g=Q(b[0].type);if(!e.android){var h=
|
||||
!1;b.on("compositionstart",function(){h=!0});b.on("compositionend",function(){h=!1;l()})}var k,l=function(a){k&&(f.defer.cancel(k),k=null);if(!h){var e=b.val();a=a&&a.type;"password"===g||d.ngTrim&&"false"===d.ngTrim||(e=W(e));(c.$viewValue!==e||""===e&&c.$$hasNativeValidators)&&c.$setViewValue(e,a)}};if(e.hasEvent("input"))b.on("input",l);else{var m=function(a,b,c){k||(k=f.defer(function(){k=null;b&&b.value===c||l(a)}))};b.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||
|
||||
m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut",m)}b.on("change",l);if(Ld[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!k){var b=this.validity,c=b.badInput,d=b.typeMismatch;k=f.defer(function(){k=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Nb(a,b){return function(d,c){var e,f;if(da(d))return d;if(G(d)){'"'==d.charAt(0)&&'"'==d.charAt(d.length-
|
||||
1)&&(d=d.substring(1,d.length-1));if(Bg.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},q(e,function(a,c){c<b.length&&(f[b[c]]=+a)}),new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0)}return NaN}}function mb(a,b,d,c){return function(e,f,g,h,k,l,m){function n(a){return a&&!(a.getTime&&
|
||||
a.getTime()!==a.getTime())}function p(a){return w(a)&&!da(a)?d(a)||void 0:a}Md(e,f,g,h);lb(e,f,g,h,k,l);var u=h&&h.$options&&h.$options.timezone,q;h.$$parserName=a;h.$parsers.push(function(a){if(h.$isEmpty(a))return null;if(b.test(a))return a=d(a,q),u&&(a=Sb(a,u)),a});h.$formatters.push(function(a){if(a&&!da(a))throw nb("datefmt",a);if(n(a))return(q=a)&&u&&(q=Sb(q,u,!0)),m("date")(a,c,u);q=null;return""});if(w(g.min)||g.ngMin){var s;h.$validators.min=function(a){return!n(a)||y(s)||d(a)>=s};g.$observe("min",
|
||||
function(a){s=p(a);h.$validate()})}if(w(g.max)||g.ngMax){var r;h.$validators.max=function(a){return!n(a)||y(r)||d(a)<=r};g.$observe("max",function(a){r=p(a);h.$validate()})}}}function Md(a,b,d,c){(c.$$hasNativeValidators=D(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{};return c.badInput||c.typeMismatch?void 0:a})}function Nd(a,b,d,c,e){if(w(c)){a=a(c);if(!a.constant)throw nb("constexpr",d,c);return a(b)}return e}function qc(a,b){a="ngClass"+a;return["$animate",function(d){function c(a,
|
||||
b){var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],m=0;m<b.length;m++)if(e==b[m])continue a;c.push(e)}return c}function e(a){var b=[];return L(a)?(q(a,function(a){b=b.concat(e(a))}),b):G(a)?a.split(" "):D(a)?(q(a,function(a,c){a&&(b=b.concat(c.split(" ")))}),b):a}return{restrict:"AC",link:function(f,g,h){function k(a){a=l(a,1);h.$addClass(a)}function l(a,b){var c=g.data("$classCounts")||U(),d=[];q(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});g.data("$classCounts",c);return d.join(" ")}
|
||||
function m(a,b){var e=c(b,a),f=c(a,b),e=l(e,1),f=l(f,-1);e&&e.length&&d.addClass(g,e);f&&f.length&&d.removeClass(g,f)}function n(a){if(!0===b||(f.$index&1)===b){var c=e(a||[]);if(!p)k(c);else if(!na(a,p)){var d=e(p);m(d,c)}}p=L(a)?a.map(function(a){return ia(a)}):ia(a)}var p;f.$watch(h[a],n,!0);h.$observe("class",function(b){n(f.$eval(h[a]))});"ngClass"!==a&&f.$watch("$index",function(c,d){var g=c&1;if(g!==(d&1)){var m=e(f.$eval(h[a]));g===b?k(m):(g=l(m,-1),h.$removeClass(g))}})}}}]}function Kd(a){function b(a,
|
||||
b){b&&!f[a]?(k.addClass(e,a),f[a]=!0):!b&&f[a]&&(k.removeClass(e,a),f[a]=!1)}function d(a,c){a=a?"-"+Cc(a,"-"):"";b(ob+a,!0===c);b(Od+a,!1===c)}var c=a.ctrl,e=a.$element,f={},g=a.set,h=a.unset,k=a.$animate;f[Od]=!(f[ob]=e.hasClass(ob));c.$setValidity=function(a,e,f){y(e)?(c.$pending||(c.$pending={}),g(c.$pending,a,f)):(c.$pending&&h(c.$pending,a,f),Pd(c.$pending)&&(c.$pending=void 0));Ga(e)?e?(h(c.$error,a,f),g(c.$$success,a,f)):(g(c.$error,a,f),h(c.$$success,a,f)):(h(c.$error,a,f),h(c.$$success,
|
||||
a,f));c.$pending?(b(Qd,!0),c.$valid=c.$invalid=void 0,d("",null)):(b(Qd,!1),c.$valid=Pd(c.$error),c.$invalid=!c.$valid,d("",c.$valid));e=c.$pending&&c.$pending[a]?void 0:c.$error[a]?!1:c.$$success[a]?!0:null;d(a,e);c.$$parentForm.$setValidity(a,e,c)}}function Pd(a){if(a)for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}var Cg=/^\/(.+)\/([a-z]*)$/,ua=Object.prototype.hasOwnProperty,Q=function(a){return G(a)?a.toLowerCase():a},ub=function(a){return G(a)?a.toUpperCase():a},Ea,F,qa,va=[].slice,
|
||||
bg=[].splice,Dg=[].push,ma=Object.prototype.toString,wc=Object.getPrototypeOf,xa=N("ng"),ca=C.angular||(C.angular={}),Ub,pb=0;Ea=C.document.documentMode;A.$inject=[];Xa.$inject=[];var L=Array.isArray,ae=/^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/,W=function(a){return G(a)?a.trim():a},wd=function(a){return a.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")},Ba=function(){if(!w(Ba.rules)){var a=C.document.querySelector("[ng-csp]")||
|
||||
C.document.querySelector("[data-ng-csp]");if(a){var b=a.getAttribute("ng-csp")||a.getAttribute("data-ng-csp");Ba.rules={noUnsafeEval:!b||-1!==b.indexOf("no-unsafe-eval"),noInlineStyle:!b||-1!==b.indexOf("no-inline-style")}}else{a=Ba;try{new Function(""),b=!1}catch(d){b=!0}a.rules={noUnsafeEval:b,noInlineStyle:!1}}}return Ba.rules},rb=function(){if(w(rb.name_))return rb.name_;var a,b,d=Na.length,c,e;for(b=0;b<d;++b)if(c=Na[b],a=C.document.querySelector("["+c.replace(":","\\:")+"jq]")){e=a.getAttribute(c+
|
||||
"jq");break}return rb.name_=e},de=/:/g,Na=["ng-","data-ng-","ng:","x-ng-"],ie=/[A-Z]/g,Dc=!1,Ma=3,me={full:"1.5.8",major:1,minor:5,dot:8,codeName:"arbitrary-fallbacks"};O.expando="ng339";var fb=O.cache={},Pf=1;O._data=function(a){return this.cache[a[this.expando]]||{}};var Kf=/([\:\-\_]+(.))/g,Lf=/^moz([A-Z])/,yb={mouseleave:"mouseout",mouseenter:"mouseover"},Wb=N("jqLite"),Of=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,Vb=/<|&#?\w+;/,Mf=/<([\w:-]+)/,Nf=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,
|
||||
ja={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ja.optgroup=ja.option;ja.tbody=ja.tfoot=ja.colgroup=ja.caption=ja.thead;ja.th=ja.td;var Uf=C.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Oa=O.prototype={ready:function(a){function b(){d||(d=!0,a())}var d=!1;"complete"===
|
||||
C.document.readyState?C.setTimeout(b):(this.on("DOMContentLoaded",b),O(C).on("load",b))},toString:function(){var a=[];q(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?F(this[a]):F(this[this.length+a])},length:0,push:Dg,sort:[].sort,splice:[].splice},Eb={};q("multiple selected checked disabled readOnly required open".split(" "),function(a){Eb[Q(a)]=a});var Vc={};q("input select option textarea button form details".split(" "),function(a){Vc[a]=!0});var bd={ngMinlength:"minlength",
|
||||
ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};q({data:Yb,removeData:eb,hasData:function(a){for(var b in fb[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b<d;b++)eb(a[b])}},function(a,b){O[b]=a});q({data:Yb,inheritedData:Cb,scope:function(a){return F.data(a,"$scope")||Cb(a.parentNode||a,["$isolateScope","$scope"])},isolateScope:function(a){return F.data(a,"$isolateScope")||F.data(a,"$isolateScopeNoTemplate")},controller:Sc,injector:function(a){return Cb(a,
|
||||
"$injector")},removeAttr:function(a,b){a.removeAttribute(b)},hasClass:zb,css:function(a,b,d){b=db(b);if(w(d))a.style[b]=d;else return a.style[b]},attr:function(a,b,d){var c=a.nodeType;if(c!==Ma&&2!==c&&8!==c)if(c=Q(b),Eb[c])if(w(d))d?(a[b]=!0,a.setAttribute(b,c)):(a[b]=!1,a.removeAttribute(c));else return a[b]||(a.attributes.getNamedItem(b)||A).specified?c:void 0;else if(w(d))a.setAttribute(b,d);else if(a.getAttribute)return a=a.getAttribute(b,2),null===a?void 0:a},prop:function(a,b,d){if(w(d))a[b]=
|
||||
d;else return a[b]},text:function(){function a(a,d){if(y(d)){var c=a.nodeType;return 1===c||c===Ma?a.textContent:""}a.textContent=d}a.$dv="";return a}(),val:function(a,b){if(y(b)){if(a.multiple&&"select"===wa(a)){var d=[];q(a.options,function(a){a.selected&&d.push(a.value||a.text)});return 0===d.length?null:d}return a.value}a.value=b},html:function(a,b){if(y(b))return a.innerHTML;wb(a,!0);a.innerHTML=b},empty:Tc},function(a,b){O.prototype[b]=function(b,c){var e,f,g=this.length;if(a!==Tc&&y(2==a.length&&
|
||||
a!==zb&&a!==Sc?b:c)){if(D(b)){for(e=0;e<g;e++)if(a===Yb)a(this[e],b);else for(f in b)a(this[e],f,b[f]);return this}e=a.$dv;g=y(e)?Math.min(g,1):g;for(f=0;f<g;f++){var h=a(this[f],b,c);e=e?e+h:h}return e}for(e=0;e<g;e++)a(this[e],b,c);return this}});q({removeData:eb,on:function(a,b,d,c){if(w(c))throw Wb("onargs");if(Nc(a)){c=xb(a,!0);var e=c.events,f=c.handle;f||(f=c.handle=Rf(a,e));c=0<=b.indexOf(" ")?b.split(" "):[b];for(var g=c.length,h=function(b,c,g){var h=e[b];h||(h=e[b]=[],h.specialHandlerWrapper=
|
||||
c,"$destroy"===b||g||a.addEventListener(b,f,!1));h.push(d)};g--;)b=c[g],yb[b]?(h(yb[b],Tf),h(b,void 0,!0)):h(b)}},off:Rc,one:function(a,b,d){a=F(a);a.on(b,function e(){a.off(b,d);a.off(b,e)});a.on(b,d)},replaceWith:function(a,b){var d,c=a.parentNode;wb(a);q(new O(b),function(b){d?c.insertBefore(b,d.nextSibling):c.replaceChild(b,a);d=b})},children:function(a){var b=[];q(a.childNodes,function(a){1===a.nodeType&&b.push(a)});return b},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,
|
||||
b){var d=a.nodeType;if(1===d||11===d){b=new O(b);for(var d=0,c=b.length;d<c;d++)a.appendChild(b[d])}},prepend:function(a,b){if(1===a.nodeType){var d=a.firstChild;q(new O(b),function(b){a.insertBefore(b,d)})}},wrap:function(a,b){Pc(a,F(b).eq(0).clone()[0])},remove:Db,detach:function(a){Db(a,!0)},after:function(a,b){var d=a,c=a.parentNode;b=new O(b);for(var e=0,f=b.length;e<f;e++){var g=b[e];c.insertBefore(g,d.nextSibling);d=g}},addClass:Bb,removeClass:Ab,toggleClass:function(a,b,d){b&&q(b.split(" "),
|
||||
function(b){var e=d;y(e)&&(e=!zb(a,b));(e?Bb:Ab)(a,b)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,b){return a.getElementsByTagName?a.getElementsByTagName(b):[]},clone:Xb,triggerHandler:function(a,b,d){var c,e,f=b.type||b,g=xb(a);if(g=(g=g&&g.events)&&g[f])c={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=
|
||||
!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:A,type:f,target:a},b.type&&(c=S(c,b)),b=ia(g),e=d?[c].concat(d):[c],q(b,function(b){c.isImmediatePropagationStopped()||b.apply(a,e)})}},function(a,b){O.prototype[b]=function(b,c,e){for(var f,g=0,h=this.length;g<h;g++)y(f)?(f=a(this[g],b,c,e),w(f)&&(f=F(f))):Qc(f,a(this[g],b,c,e));return w(f)?f:this};O.prototype.bind=O.prototype.on;O.prototype.unbind=O.prototype.off});Ra.prototype={put:function(a,
|
||||
b){this[Ca(a,this.nextUid)]=b},get:function(a){return this[Ca(a,this.nextUid)]},remove:function(a){var b=this[a=Ca(a,this.nextUid)];delete this[a];return b}};var If=[function(){this.$get=[function(){return Ra}]}],Wf=/^([^\(]+?)=>/,Xf=/^[^\(]*\(\s*([^\)]*)\)/m,Eg=/,/,Fg=/^\s*(_?)(\S+?)\1\s*$/,Vf=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ha=N("$injector");cb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw G(d)&&d||(d=a.name||Yf(a)),Ha("strictdi",d);
|
||||
b=Wc(a);q(b[1].split(Eg),function(a){a.replace(Fg,function(a,b,d){c.push(d)})})}a.$inject=c}}else L(a)?(b=a.length-1,Pa(a[b],"fn"),c=a.slice(0,b)):Pa(a,"fn",!0);return c};var Rd=N("$animate"),$e=function(){this.$get=A},af=function(){var a=new Ra,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=G(b)?b.split(" "):L(b)?b:[],q(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){q(b,function(b){var c=a.get(b);if(c){var d=Zf(b.attr("class")),e="",f="";q(c,
|
||||
function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});q(b,function(a){e&&Bb(a,e);f&&Ab(a,f)});a.remove(b)}});b.length=0}return{enabled:A,on:A,off:A,pin:A,push:function(g,h,k,l){l&&l();k=k||{};k.from&&g.css(k.from);k.to&&g.css(k.to);if(k.addClass||k.removeClass)if(h=k.addClass,l=k.removeClass,k=a.get(g)||{},h=e(k,h,!0),l=e(k,l,!1),h||l)a.put(g,k),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},Ye=["$provide",function(a){var b=this;this.$$registeredAnimations=
|
||||
Object.create(null);this.register=function(d,c){if(d&&"."!==d.charAt(0))throw Rd("notcsel",d);var e=d+"-animation";b.$$registeredAnimations[d.substr(1)]=e;a.factory(e,c)};this.classNameFilter=function(a){if(1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?a:null)&&/(\s+|\/)ng-animate(\s+|\/)/.test(this.$$classNameFilter.toString()))throw Rd("nongcls","ng-animate");return this.$$classNameFilter};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var h;a:{for(h=0;h<d.length;h++){var k=
|
||||
d[h];if(1===k.nodeType){h=k;break a}}h=void 0}!h||h.parentNode||h.previousElementSibling||(d=null)}d?d.after(a):c.prepend(a)}return{on:a.on,off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.end&&a.end()},enter:function(e,f,g,h){f=f&&F(f);g=g&&F(g);f=f||g.parent();b(e,f,g);return a.push(e,"enter",Ia(h))},move:function(e,f,g,h){f=f&&F(f);g=g&&F(g);f=f||g.parent();b(e,f,g);return a.push(e,"move",Ia(h))},leave:function(b,c){return a.push(b,"leave",Ia(c),function(){b.remove()})},addClass:function(b,
|
||||
c,g){g=Ia(g);g.addClass=gb(g.addclass,c);return a.push(b,"addClass",g)},removeClass:function(b,c,g){g=Ia(g);g.removeClass=gb(g.removeClass,c);return a.push(b,"removeClass",g)},setClass:function(b,c,g,h){h=Ia(h);h.addClass=gb(h.addClass,c);h.removeClass=gb(h.removeClass,g);return a.push(b,"setClass",h)},animate:function(b,c,g,h,k){k=Ia(k);k.from=k.from?S(k.from,c):c;k.to=k.to?S(k.to,g):g;k.tempClasses=gb(k.tempClasses,h||"ng-inline-animate");return a.push(b,"animate",k)}}}]}],cf=function(){this.$get=
|
||||
["$$rAF",function(a){function b(b){d.push(b);1<d.length||a(function(){for(var a=0;a<d.length;a++)d[a]();d=[]})}var d=[];return function(){var a=!1;b(function(){a=!0});return function(d){a?d():b(d)}}}]},bf=function(){this.$get=["$q","$sniffer","$$animateAsyncRun","$document","$timeout",function(a,b,d,c,e){function f(a){this.setHost(a);var b=d();this._doneCallbacks=[];this._tick=function(a){var d=c[0];d&&d.hidden?e(a,0,!1):b(a)};this._state=0}f.chain=function(a,b){function c(){if(d===a.length)b(!0);
|
||||
else a[d](function(a){!1===a?b(!1):(d++,c())})}var d=0;c()};f.all=function(a,b){function c(f){e=e&&f;++d===a.length&&b(e)}var d=0,e=!0;q(a,function(a){a.done(c)})};f.prototype={setHost:function(a){this.host=a||{}},done:function(a){2===this._state?a():this._doneCallbacks.push(a)},progress:A,getPromise:function(){if(!this.promise){var b=this;this.promise=a(function(a,c){b.done(function(b){!1===b?c():a()})})}return this.promise},then:function(a,b){return this.getPromise().then(a,b)},"catch":function(a){return this.getPromise()["catch"](a)},
|
||||
"finally":function(a){return this.getPromise()["finally"](a)},pause:function(){this.host.pause&&this.host.pause()},resume:function(){this.host.resume&&this.host.resume()},end:function(){this.host.end&&this.host.end();this._resolve(!0)},cancel:function(){this.host.cancel&&this.host.cancel();this._resolve(!1)},complete:function(a){var b=this;0===b._state&&(b._state=1,b._tick(function(){b._resolve(a)}))},_resolve:function(a){2!==this._state&&(q(this._doneCallbacks,function(b){b(a)}),this._doneCallbacks.length=
|
||||
0,this._state=2)}};return f}]},Ze=function(){this.$get=["$$rAF","$q","$$AnimateRunner",function(a,b,d){return function(b,e){function f(){a(function(){g.addClass&&(b.addClass(g.addClass),g.addClass=null);g.removeClass&&(b.removeClass(g.removeClass),g.removeClass=null);g.to&&(b.css(g.to),g.to=null);h||k.complete();h=!0});return k}var g=e||{};g.$$prepared||(g=pa(g));g.cleanupStyles&&(g.from=g.to=null);g.from&&(b.css(g.from),g.from=null);var h,k=new d;return{start:f,end:f}}}]},ga=N("$compile"),bc=new function(){};
|
||||
Fc.$inject=["$provide","$$sanitizeUriProvider"];Fb.prototype.isFirstChange=function(){return this.previousValue===bc};var Yc=/^((?:x|data)[\:\-_])/i,cg=N("$controller"),cd=/^(\S+)(\s+as\s+([\w$]+))?$/,jf=function(){this.$get=["$document",function(a){return function(b){b?!b.nodeType&&b instanceof F&&(b=b[0]):b=a[0].body;return b.offsetWidth+1}}]},dd="application/json",ec={"Content-Type":dd+";charset=utf-8"},eg=/^\[|^\{(?!\{)/,fg={"[":/]$/,"{":/}$/},dg=/^\)\]\}',?\n/,Gg=N("$http"),hd=function(a){return function(){throw Gg("legacy",
|
||||
a);}},Ka=ca.$interpolateMinErr=N("$interpolate");Ka.throwNoconcat=function(a){throw Ka("noconcat",a);};Ka.interr=function(a,b){return Ka("interr",a,b.toString())};var rf=function(){this.$get=["$window",function(a){function b(a){var b=function(a){b.data=a;b.called=!0};b.id=a;return b}var d=a.angular.callbacks,c={};return{createCallback:function(a){a="_"+(d.$$counter++).toString(36);var f="angular.callbacks."+a,g=b(a);c[f]=d[a]=g;return f},wasCalled:function(a){return c[a].called},getResponse:function(a){return c[a].data},
|
||||
removeCallback:function(a){delete d[c[a].id];delete c[a]}}}]},Hg=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,hg={http:80,https:443,ftp:21},Gb=N("$location"),Ig={$$absUrl:"",$$html5:!1,$$replace:!1,absUrl:Hb("$$absUrl"),url:function(a){if(y(a))return this.$$url;var b=Hg.exec(a);(b[1]||""===a)&&this.path(decodeURIComponent(b[1]));(b[2]||b[1]||""===a)&&this.search(b[3]||"");this.hash(b[5]||"");return this},protocol:Hb("$$protocol"),host:Hb("$$host"),port:Hb("$$port"),path:md("$$path",function(a){a=null!==a?a.toString():
|
||||
"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,b){switch(arguments.length){case 0:return this.$$search;case 1:if(G(a)||T(a))a=a.toString(),this.$$search=Ac(a);else if(D(a))a=pa(a,{}),q(a,function(b,c){null==b&&delete a[c]}),this.$$search=a;else throw Gb("isrcharg");break;default:y(b)||null===b?delete this.$$search[a]:this.$$search[a]=b}this.$$compose();return this},hash:md("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};q([ld,hc,gc],
|
||||
function(a){a.prototype=Object.create(Ig);a.prototype.state=function(b){if(!arguments.length)return this.$$state;if(a!==gc||!this.$$html5)throw Gb("nostate");this.$$state=y(b)?null:b;return this}});var X=N("$parse"),jg=Function.prototype.call,kg=Function.prototype.apply,lg=Function.prototype.bind,Ob=U();q("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Ob[a]=!0});var Jg={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},jc=function(a){this.options=a};jc.prototype={constructor:jc,
|
||||
lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdentifierStart(this.peekMultichar()))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var b=a+this.peek(),d=b+this.peek(2),c=Ob[b],e=Ob[d];Ob[a]||
|
||||
c||e?(a=e?d:c?b:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,b){return-1!==b.indexOf(a)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?
|
||||
this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):
|
||||
(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=w(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw X("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index<
|
||||
this.text.length;){var d=Q(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var c=this.peek();if("e"==d&&this.isExpOperator(c))a+=d;else if(this.isExpOperator(d)&&c&&this.isNumber(c)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||c&&this.isNumber(c)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:b,text:a,constant:!0,value:Number(a)})},readIdent:function(){var a=this.index;for(this.index+=this.peekMultichar().length;this.index<
|
||||
this.text.length;){var b=this.peekMultichar();if(!this.isIdentifierContinue(b))break;this.index+=b.length}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var b=this.index;this.index++;for(var d="",c=a,e=!1;this.index<this.text.length;){var f=this.text.charAt(this.index),c=c+f;if(e)"u"===f?(e=this.text.substring(this.index+1,this.index+5),e.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+e+"]"),this.index+=4,d+=String.fromCharCode(parseInt(e,
|
||||
16))):d+=Jg[f]||f,e=!1;else if("\\"===f)e=!0;else{if(f===a){this.index++;this.tokens.push({index:b,text:c,constant:!0,value:d});return}d+=f}this.index++}this.throwError("Unterminated quote",b)}};var s=function(a,b){this.lexer=a;this.options=b};s.Program="Program";s.ExpressionStatement="ExpressionStatement";s.AssignmentExpression="AssignmentExpression";s.ConditionalExpression="ConditionalExpression";s.LogicalExpression="LogicalExpression";s.BinaryExpression="BinaryExpression";s.UnaryExpression="UnaryExpression";
|
||||
s.CallExpression="CallExpression";s.MemberExpression="MemberExpression";s.Identifier="Identifier";s.Literal="Literal";s.ArrayExpression="ArrayExpression";s.Property="Property";s.ObjectExpression="ObjectExpression";s.ThisExpression="ThisExpression";s.LocalsExpression="LocalsExpression";s.NGValueParameter="NGValueParameter";s.prototype={ast:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},
|
||||
program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:s.Program,body:a}},expressionStatement:function(){return{type:s.ExpressionStatement,expression:this.filterChain()}},filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();this.expect("=")&&(a={type:s.AssignmentExpression,
|
||||
left:a,right:this.assignment(),operator:"="});return a},ternary:function(){var a=this.logicalOR(),b,d;return this.expect("?")&&(b=this.expression(),this.consume(":"))?(d=this.expression(),{type:s.ConditionalExpression,test:a,alternate:b,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a={type:s.LogicalExpression,operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a={type:s.LogicalExpression,
|
||||
operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),b;b=this.expect("==","!=","===","!==");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.relational()};return a},relational:function(){for(var a=this.additive(),b;b=this.expect("<",">","<=",">=");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression,operator:b.text,
|
||||
left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():
|
||||
this.selfReferential.hasOwnProperty(this.peek().text)?a=pa(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:s.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):
|
||||
"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))
|
||||
}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;
|
||||
b={type:s.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");
|
||||
return{type:s.ObjectExpression,properties:a}},throwError:function(a,b){throw X("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw X("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw X("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>
|
||||
a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:s.ThisExpression},$locals:{type:s.LocalsExpression}}};td.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.state={nextId:0,filters:{},expensiveChecks:b,fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};V(c,d.$filter);var e="",f;this.stage="assign";
|
||||
if(f=rd(c))this.state.computing="assign",e=this.nextId(),this.recurse(f,e),this.return_(e),e="fn.assign="+this.generateFunction("assign","s,v,l");f=pd(c.body);d.stage="inputs";q(f,function(a,b){var c="fn"+b;d.state[c]={vars:[],body:[],own:{}};d.state.computing=c;var e=d.nextId();d.recurse(a,e);d.return_(e);d.state.inputs.push(c);a.watchId=b});this.state.computing="fn";this.stage="main";this.recurse(c);e='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+
|
||||
e+this.watchFns()+"return fn;";e=(new Function("$filter","ensureSafeMemberName","ensureSafeObject","ensureSafeFunction","getStringValue","ensureSafeAssignContext","ifDefined","plus","text",e))(this.$filter,Sa,ra,nd,ig,Ib,mg,od,a);this.state=this.stage=void 0;e.literal=sd(c);e.constant=c.constant;return e},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;q(b,function(b){a.push("var "+b+"="+d.generateFunction(b,"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");
|
||||
return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;q(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,b,d,c,e,f){var g,h,k=this,l,m,n;c=c||A;if(!f&&w(a.watchId))b=
|
||||
b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case s.Program:q(a.body,function(b,c){k.recurse(b.expression,void 0,void 0,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(m);break;case s.UnaryExpression:this.recurse(a.argument,void 0,void 0,function(a){h=a});m=a.operator+"("+this.ifDefined(h,0)+")";this.assign(b,m);
|
||||
c(m);break;case s.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){h=a});m="+"===a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right,b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test,
|
||||
b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);Sa(a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.not(k.nonComputedMember("s",a.name)),k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s",
|
||||
a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));(k.state.expensiveChecks||Jb(a.name))&&k.addEnsureSafeObject(b);c(b);break;case s.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,void 0,function(){k.if_(k.notNull(g),function(){e&&1!==e&&k.addEnsureSafeAssignContext(g);if(a.computed)h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),k.addEnsureSafeMemberName(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,
|
||||
h),"{}")),m=k.ensureSafeObject(k.computedMember(g,h)),k.assign(b,m),d&&(d.computed=!0,d.name=h);else{Sa(a.property.name);e&&1!==e&&k.if_(k.not(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}"));m=k.nonComputedMember(g,a.property.name);if(k.state.expensiveChecks||Jb(a.property.name))m=k.ensureSafeObject(m);k.assign(b,m);d&&(d.computed=!1,d.name=a.property.name)}},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();
|
||||
a.filter?(h=k.filter(a.callee.name),l=[],q(a.arguments,function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+l.join(",")+")",k.assign(b,m),c(b)):(h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){k.addEnsureSafeFunction(h);q(a.arguments,function(a){k.recurse(a,k.nextId(),void 0,function(a){l.push(k.ensureSafeObject(a))})});g.name?(k.state.expensiveChecks||k.addEnsureSafeObject(g.context),m=k.member(g.context,g.name,g.computed)+"("+l.join(",")+")"):m=
|
||||
h+"("+l.join(",")+")";m=k.ensureSafeObject(m);k.assign(b,m)},function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=this.nextId();g={};if(!qd(a.left))throw X("lval");this.recurse(a.left,void 0,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);k.addEnsureSafeObject(k.member(g.context,g.name,g.computed));k.addEnsureSafeAssignContext(g.context);m=k.member(g.context,g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=
|
||||
[];q(a.elements,function(a){k.recurse(a,k.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(m);break;case s.ObjectExpression:l=[];n=!1;q(a.properties,function(a){a.computed&&(n=!0)});n?(b=b||this.nextId(),this.assign(b,"{}"),q(a.properties,function(a){a.computed?(g=k.nextId(),k.recurse(a.key,g)):g=a.key.type===s.Identifier?a.key.name:""+a.key.value;h=k.nextId();k.recurse(a.value,h);k.assign(k.member(b,g,a.computed),h)})):(q(a.properties,function(b){k.recurse(b.value,
|
||||
a.constant?void 0:k.nextId(),void 0,function(a){l.push(k.escape(b.key.type===s.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case s.ThisExpression:this.assign(b,"s");c("s");break;case s.LocalsExpression:this.assign(b,"l");c("l");break;case s.NGValueParameter:this.assign(b,"v"),c("v")}},getHasOwnProperty:function(a,b){var d=a+"."+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},
|
||||
assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),
|
||||
d(),c.push("}"))}},not:function(a){return"!("+a+")"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/[$_a-zA-Z][$_a-zA-Z0-9]*/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,b)},addEnsureSafeObject:function(a){this.current().body.push(this.ensureSafeObject(a),";")},addEnsureSafeMemberName:function(a){this.current().body.push(this.ensureSafeMemberName(a),
|
||||
";")},addEnsureSafeFunction:function(a){this.current().body.push(this.ensureSafeFunction(a),";")},addEnsureSafeAssignContext:function(a){this.current().body.push(this.ensureSafeAssignContext(a),";")},ensureSafeObject:function(a){return"ensureSafeObject("+a+",text)"},ensureSafeMemberName:function(a){return"ensureSafeMemberName("+a+",text)"},ensureSafeFunction:function(a){return"ensureSafeFunction("+a+",text)"},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},ensureSafeAssignContext:function(a){return"ensureSafeAssignContext("+
|
||||
a+",text)"},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(G(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(T(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"===
|
||||
typeof a)return"undefined";throw X("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};ud.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.expression=a;this.expensiveChecks=b;V(c,d.$filter);var e,f;if(e=rd(c))f=this.recurse(e);e=pd(c.body);var g;e&&(g=[],q(e,function(a,b){var c=d.recurse(a);a.input=c;g.push(c);a.watchId=b}));var h=[];q(c.body,function(a){h.push(d.recurse(a.expression))});
|
||||
e=0===c.body.length?A:1===c.body.length?h[0]:function(a,b){var c;q(h,function(d){c=d(a,b)});return c};f&&(e.assign=function(a,b,c){return f(a,c,b)});g&&(e.inputs=g);e.literal=sd(c);e.constant=c.constant;return e},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left),
|
||||
e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return Sa(a.name,f.expression),f.identifier(a.name,f.expensiveChecks||Jb(a.name),b,d,f.expression);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(Sa(a.property.name,
|
||||
f.expression),e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d,f.expression):this.nonComputedMember(c,e,f.expensiveChecks,b,d,f.expression);case s.CallExpression:return g=[],q(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var n=[],p=0;p<g.length;++p)n.push(g[p](a,c,d,f));a=e.apply(void 0,n,f);return b?{context:void 0,name:void 0,value:a}:
|
||||
a}:function(a,c,d,m){var n=e(a,c,d,m),p;if(null!=n.value){ra(n.context,f.expression);nd(n.value,f.expression);p=[];for(var q=0;q<g.length;++q)p.push(ra(g[q](a,c,d,m),f.expression));p=ra(n.value.apply(n.context,p),f.expression)}return b?{value:p}:p};case s.AssignmentExpression:return c=this.recurse(a.left,!0,1),e=this.recurse(a.right),function(a,d,g,m){var n=c(a,d,g,m);a=e(a,d,g,m);ra(n.value,f.expression);Ib(n.context);n.context[n.name]=a;return b?{value:a}:a};case s.ArrayExpression:return g=[],q(a.elements,
|
||||
function(a){g.push(f.recurse(a))}),function(a,c,d,e){for(var f=[],p=0;p<g.length;++p)f.push(g[p](a,c,d,e));return b?{value:f}:f};case s.ObjectExpression:return g=[],q(a.properties,function(a){a.computed?g.push({key:f.recurse(a.key),computed:!0,value:f.recurse(a.value)}):g.push({key:a.key.type===s.Identifier?a.key.name:""+a.key.value,computed:!1,value:f.recurse(a.value)})}),function(a,c,d,e){for(var f={},p=0;p<g.length;++p)g[p].computed?f[g[p].key(a,c,d,e)]=g[p].value(a,c,d,e):f[g[p].key]=g[p].value(a,
|
||||
c,d,e);return b?{value:f}:f};case s.ThisExpression:return function(a){return b?{value:a}:a};case s.LocalsExpression:return function(a,c){return b?{value:c}:c};case s.NGValueParameter:return function(a,c,d){return b?{value:d}:d}}},"unary+":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=w(d)?+d:0;return b?{value:d}:d}},"unary-":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=w(d)?-d:0;return b?{value:d}:d}},"unary!":function(a,b){return function(d,c,e,f){d=!a(d,c,e,f);return b?{value:d}:
|
||||
d}},"binary+":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);h=od(h,c);return d?{value:h}:h}},"binary-":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);h=(w(h)?h:0)-(w(c)?c:0);return d?{value:h}:h}},"binary*":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)*b(c,e,f,g);return d?{value:c}:c}},"binary/":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)/b(c,e,f,g);return d?{value:c}:c}},"binary%":function(a,b,d){return function(c,e,f,g){c=a(c,e,
|
||||
f,g)%b(c,e,f,g);return d?{value:c}:c}},"binary===":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)===b(c,e,f,g);return d?{value:c}:c}},"binary!==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!==b(c,e,f,g);return d?{value:c}:c}},"binary==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)==b(c,e,f,g);return d?{value:c}:c}},"binary!=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!=b(c,e,f,g);return d?{value:c}:c}},"binary<":function(a,b,d){return function(c,e,f,g){c=a(c,e,
|
||||
f,g)<b(c,e,f,g);return d?{value:c}:c}},"binary>":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||
|
||||
b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d,c,e){return function(f,g,h,k){f=g&&a in g?g:f;c&&1!==c&&f&&!f[a]&&(f[a]={});g=f?f[a]:void 0;b&&ra(g,e);return d?{context:f,name:a,value:g}:g}},computedMember:function(a,b,d,c,e){return function(f,g,h,k){var l=a(f,g,h,k),m,n;null!=l&&(m=b(f,
|
||||
g,h,k),m+="",Sa(m,e),c&&1!==c&&(Ib(l),l&&!l[m]&&(l[m]={})),n=l[m],ra(n,e));return d?{context:l,name:m,value:n}:n}},nonComputedMember:function(a,b,d,c,e,f){return function(g,h,k,l){g=a(g,h,k,l);e&&1!==e&&(Ib(g),g&&!g[b]&&(g[b]={}));h=null!=g?g[b]:void 0;(d||Jb(b))&&ra(h,f);return c?{context:g,name:b,value:h}:h}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};var kc=function(a,b,d){this.lexer=a;this.$filter=b;this.options=d;this.ast=new s(a,d);this.astCompiler=d.csp?new ud(this.ast,
|
||||
b):new td(this.ast,b)};kc.prototype={constructor:kc,parse:function(a){return this.astCompiler.compile(a,this.options.expensiveChecks)}};var ng=Object.prototype.valueOf,sa=N("$sce"),la={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},pg=N("$compile"),$=C.document.createElement("a"),yd=Y(C.location.href);zd.$inject=["$document"];Mc.$inject=["$provide"];var Gd=22,Fd=".",mc="0";Ad.$inject=["$locale"];Cd.$inject=["$locale"];var Ag={yyyy:ba("FullYear",4,0,!1,!0),yy:ba("FullYear",2,0,
|
||||
!0,!0),y:ba("FullYear",1,0,!1,!0),MMMM:kb("Month"),MMM:kb("Month",!0),MM:ba("Month",2,1),M:ba("Month",1,1),LLLL:kb("Month",!1,!0),dd:ba("Date",2),d:ba("Date",1),HH:ba("Hours",2),H:ba("Hours",1),hh:ba("Hours",2,-12),h:ba("Hours",1,-12),mm:ba("Minutes",2),m:ba("Minutes",1),ss:ba("Seconds",2),s:ba("Seconds",1),sss:ba("Milliseconds",3),EEEE:kb("Day"),EEE:kb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Kb(Math[0<a?"floor":"ceil"](a/
|
||||
60),2)+Kb(Math.abs(a%60),2))},ww:Id(2),w:Id(1),G:nc,GG:nc,GGG:nc,GGGG:function(a,b){return 0>=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},zg=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,yg=/^\-?\d+$/;Bd.$inject=["$locale"];var tg=ha(Q),ug=ha(ub);Dd.$inject=["$parse"];var oe=ha({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===ma.call(b.prop("href"))?
|
||||
"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),vb={};q(Eb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!=a){var c=Aa("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});vb[c]=function(){return{restrict:"A",priority:100,link:e}}}});q(bd,function(a,b){vb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"==e.ngPattern.charAt(0)&&(c=e.ngPattern.match(Cg))){e.$set("ngPattern",
|
||||
new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});q(["src","srcset","href"],function(a){var b=Aa("ng-"+a);vb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===ma.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b),Ea&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}});var Lb={$addControl:A,$$renameControl:function(a,b){a.$name=b},$removeControl:A,$setValidity:A,
|
||||
$setDirty:A,$setPristine:A,$setSubmitted:A};Jd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var Sd=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||A}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Jd,compile:function(d,f){d.addClass(Ua).addClass(ob);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in e)){var p=function(b){a.$apply(function(){n.$commitViewValue();
|
||||
n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",p,!1);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",p,!1)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n);var q=g?c(n.$name):A;g&&(q(a,n),e.$observe(g,function(b){n.$name!==b&&(q(a,void 0),n.$$parentForm.$$renameControl(n,b),q=c(n.$name),q(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n);q(a,void 0);S(n,Lb)})}}}}}]},pe=Sd(),Ce=Sd(!0),Bg=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,
|
||||
Kg=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,Lg=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/,Mg=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,Td=/^(\d{4,})-(\d{2})-(\d{2})$/,Ud=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,rc=/^(\d{4,})-W(\d\d)$/,Vd=/^(\d{4,})-(\d\d)$/,
|
||||
Wd=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Ld=U();q(["date","datetime-local","month","time","week"],function(a){Ld[a]=!0});var Xd={text:function(a,b,d,c,e,f){lb(a,b,d,c,e,f);pc(c)},date:mb("date",Td,Nb(Td,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":mb("datetimelocal",Ud,Nb(Ud,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:mb("time",Wd,Nb(Wd,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:mb("week",rc,function(a,b){if(da(a))return a;if(G(a)){rc.lastIndex=0;var d=rc.exec(a);
|
||||
if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Hd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:mb("month",Vd,Nb(Vd,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Md(a,b,d,c);lb(a,b,d,c,e,f);c.$$parserName="number";c.$parsers.push(function(a){if(c.$isEmpty(a))return null;if(Mg.test(a))return parseFloat(a)});c.$formatters.push(function(a){if(!c.$isEmpty(a)){if(!T(a))throw nb("numfmt",
|
||||
a);a=a.toString()}return a});if(w(d.min)||d.ngMin){var g;c.$validators.min=function(a){return c.$isEmpty(a)||y(g)||a>=g};d.$observe("min",function(a){w(a)&&!T(a)&&(a=parseFloat(a));g=T(a)&&!isNaN(a)?a:void 0;c.$validate()})}if(w(d.max)||d.ngMax){var h;c.$validators.max=function(a){return c.$isEmpty(a)||y(h)||a<=h};d.$observe("max",function(a){w(a)&&!T(a)&&(a=parseFloat(a));h=T(a)&&!isNaN(a)?a:void 0;c.$validate()})}},url:function(a,b,d,c,e,f){lb(a,b,d,c,e,f);pc(c);c.$$parserName="url";c.$validators.url=
|
||||
function(a,b){var d=a||b;return c.$isEmpty(d)||Kg.test(d)}},email:function(a,b,d,c,e,f){lb(a,b,d,c,e,f);pc(c);c.$$parserName="email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||Lg.test(d)}},radio:function(a,b,d,c){y(d.name)&&b.attr("name",++pb);b.on("click",function(a){b[0].checked&&c.$setViewValue(d.value,a&&a.type)});c.$render=function(){b[0].checked=d.value==c.$viewValue};d.$observe("value",c.$render)},checkbox:function(a,b,d,c,e,f,g,h){var k=Nd(h,a,"ngTrueValue",d.ngTrueValue,
|
||||
!0),l=Nd(h,a,"ngFalseValue",d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return na(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:A,button:A,submit:A,reset:A,file:A},Gc=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(Xd[Q(g.type)]||Xd.text)(e,f,
|
||||
g,h[0],b,a,d,c)}}}}],Ng=/^(true|false|\d+)$/,Ue=function(){return{restrict:"A",priority:100,compile:function(a,b){return Ng.test(b.ngValue)?function(a,b,e){e.$set("value",a.$eval(e.ngValue))}:function(a,b,e){a.$watch(e.ngValue,function(a){e.$set("value",a)})}}}},ue=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=y(a)?"":a})}}}}],we=["$interpolate","$compile",
|
||||
function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=y(a)?"":a})}}}}],ve=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=
|
||||
f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],Te=ha({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),xe=qc("",!0),ze=qc("Odd",0),ye=qc("Even",1),Ae=Ta({compile:function(a,b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),Be=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Lc={},Og={blur:!0,focus:!0};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),
|
||||
function(a){var b=Aa("ng-"+a);Lc[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g=d(f[b],null,!0);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};Og[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var Ee=["$animate","$compile",function(a,b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var h,k,l;d.$watch(e.ngIf,function(d){d?k||g(function(d,f){k=f;d[d.length++]=
|
||||
b.$$createComment("end ngIf",e.ngIf);h={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),k&&(k.$destroy(),k=null),h&&(l=tb(h.clone),a.leave(l).then(function(){l=null}),h=null))})}}}],Fe=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ca.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,p){var q=0,s,B,r,y=function(){B&&(B.remove(),B=null);s&&
|
||||
(s.$destroy(),s=null);r&&(d.leave(r).then(function(){B=null}),B=r,r=null)};c.$watch(f,function(f){var m=function(){!w(h)||h&&!c.$eval(h)||b()},t=++q;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===q){var b=c.$new();n.template=a;a=p(b,function(a){y();d.enter(a,null,e).then(m)});s=b;r=a;s.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){c.$$destroyed||t!==q||(y(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(y(),n.template=null)})}}}}],We=["$compile",function(a){return{restrict:"ECA",
|
||||
priority:-400,require:"ngInclude",link:function(b,d,c,e){ma.call(d[0]).match(/SVG/)?(d.empty(),a(Oc(e.template,C.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],Ge=Ta({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),Se=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=b.attr(d.$attr.ngList)||", ",f="false"!==d.ngTrim,g=f?W(e):e;c.$parsers.push(function(a){if(!y(a)){var b=
|
||||
[];a&&q(a.split(g),function(a){a&&b.push(f?W(a):a)});return b}});c.$formatters.push(function(a){if(L(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},ob="ng-valid",Od="ng-invalid",Ua="ng-pristine",Mb="ng-dirty",Qd="ng-pending",nb=N("ngModel"),Pg=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,b,d,c,e,f,g,h,k,l){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=void 0;this.$validators={};
|
||||
this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=void 0;this.$name=l(d.name||"",!1)(a);this.$$parentForm=Lb;var m=e(d.ngModel),n=m.assign,p=m,u=n,s=null,B,r=this;this.$$setOptions=function(a){if((r.$options=a)&&a.getterSetter){var b=e(d.ngModel+"()"),f=e(d.ngModel+"($$$p)");p=function(a){var c=m(a);z(c)&&(c=b(a));
|
||||
return c};u=function(a,b){z(m(a))?f(a,{$$$p:b}):n(a,b)}}else if(!m.assign)throw nb("nonassign",d.ngModel,ya(c));};this.$render=A;this.$isEmpty=function(a){return y(a)||""===a||null===a||a!==a};this.$$updateEmptyClasses=function(a){r.$isEmpty(a)?(f.removeClass(c,"ng-not-empty"),f.addClass(c,"ng-empty")):(f.removeClass(c,"ng-empty"),f.addClass(c,"ng-not-empty"))};var J=0;Kd({ctrl:this,$element:c,set:function(a,b){a[b]=!0},unset:function(a,b){delete a[b]},$animate:f});this.$setPristine=function(){r.$dirty=
|
||||
!1;r.$pristine=!0;f.removeClass(c,Mb);f.addClass(c,Ua)};this.$setDirty=function(){r.$dirty=!0;r.$pristine=!1;f.removeClass(c,Ua);f.addClass(c,Mb);r.$$parentForm.$setDirty()};this.$setUntouched=function(){r.$touched=!1;r.$untouched=!0;f.setClass(c,"ng-untouched","ng-touched")};this.$setTouched=function(){r.$touched=!0;r.$untouched=!1;f.setClass(c,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){g.cancel(s);r.$viewValue=r.$$lastCommittedViewValue;r.$render()};this.$validate=function(){if(!T(r.$modelValue)||
|
||||
!isNaN(r.$modelValue)){var a=r.$$rawModelValue,b=r.$valid,c=r.$modelValue,d=r.$options&&r.$options.allowInvalid;r.$$runValidators(a,r.$$lastCommittedViewValue,function(e){d||b===e||(r.$modelValue=e?a:void 0,r.$modelValue!==c&&r.$$writeModelToScope())})}};this.$$runValidators=function(a,b,c){function d(){var c=!0;q(r.$validators,function(d,e){var g=d(a,b);c=c&&g;f(e,g)});return c?!0:(q(r.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;q(r.$asyncValidators,function(e,g){var h=
|
||||
e(a,b);if(!h||!z(h.then))throw nb("nopromise",h);f(g,void 0);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?k.all(c).then(function(){g(d)},A):g(!0)}function f(a,b){h===J&&r.$setValidity(a,b)}function g(a){h===J&&c(a)}J++;var h=J;(function(){var a=r.$$parserName||"parse";if(y(B))f(a,null);else return B||(q(r.$validators,function(a,b){f(b,null)}),q(r.$asyncValidators,function(a,b){f(b,null)})),f(a,B),B;return!0})()?d()?e():g(!1):g(!1)};this.$commitViewValue=function(){var a=
|
||||
r.$viewValue;g.cancel(s);if(r.$$lastCommittedViewValue!==a||""===a&&r.$$hasNativeValidators)r.$$updateEmptyClasses(a),r.$$lastCommittedViewValue=a,r.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var b=r.$$lastCommittedViewValue;if(B=y(b)?void 0:!0)for(var c=0;c<r.$parsers.length;c++)if(b=r.$parsers[c](b),y(b)){B=!1;break}T(r.$modelValue)&&isNaN(r.$modelValue)&&(r.$modelValue=p(a));var d=r.$modelValue,e=r.$options&&r.$options.allowInvalid;r.$$rawModelValue=
|
||||
b;e&&(r.$modelValue=b,r.$modelValue!==d&&r.$$writeModelToScope());r.$$runValidators(b,r.$$lastCommittedViewValue,function(a){e||(r.$modelValue=a?b:void 0,r.$modelValue!==d&&r.$$writeModelToScope())})};this.$$writeModelToScope=function(){u(a,r.$modelValue);q(r.$viewChangeListeners,function(a){try{a()}catch(c){b(c)}})};this.$setViewValue=function(a,b){r.$viewValue=a;r.$options&&!r.$options.updateOnDefault||r.$$debounceViewValueCommit(b)};this.$$debounceViewValueCommit=function(b){var c=0,d=r.$options;
|
||||
d&&w(d.debounce)&&(d=d.debounce,T(d)?c=d:T(d[b])?c=d[b]:T(d["default"])&&(c=d["default"]));g.cancel(s);c?s=g(function(){r.$commitViewValue()},c):h.$$phase?r.$commitViewValue():a.$apply(function(){r.$commitViewValue()})};a.$watch(function(){var b=p(a);if(b!==r.$modelValue&&(r.$modelValue===r.$modelValue||b===b)){r.$modelValue=r.$$rawModelValue=b;B=void 0;for(var c=r.$formatters,d=c.length,e=b;d--;)e=c[d](e);r.$viewValue!==e&&(r.$$updateEmptyClasses(e),r.$viewValue=r.$$lastCommittedViewValue=e,r.$render(),
|
||||
r.$$runValidators(b,e,A))}return b})}],Re=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:Pg,priority:1,compile:function(b){b.addClass(Ua).addClass("ng-untouched").addClass(ob);return{pre:function(a,b,e,f){var g=f[0];b=f[1]||g.$$parentForm;g.$$setOptions(f[2]&&f[2].$options);b.$addControl(g);e.$observe("name",function(a){g.$name!==a&&g.$$parentForm.$$renameControl(g,a)});a.$on("$destroy",function(){g.$$parentForm.$removeControl(g)})},post:function(b,
|
||||
c,e,f){var g=f[0];if(g.$options&&g.$options.updateOn)c.on(g.$options.updateOn,function(a){g.$$debounceViewValueCommit(a&&a.type)});c.on("blur",function(){g.$touched||(a.$$phase?b.$evalAsync(g.$setTouched):b.$apply(g.$setTouched))})}}}}}],Qg=/(\s+|^)default(\s+|$)/,Ve=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,b){var d=this;this.$options=pa(a.$eval(b.ngModelOptions));w(this.$options.updateOn)?(this.$options.updateOnDefault=!1,this.$options.updateOn=W(this.$options.updateOn.replace(Qg,
|
||||
function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},He=Ta({terminal:!0,priority:1E3}),Rg=N("ngOptions"),Sg=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,Pe=["$compile","$document","$parse",function(a,b,d){function c(a,b,c){function e(a,b,c,d,f){this.selectValue=a;this.viewValue=
|
||||
b;this.label=c;this.group=d;this.disabled=f}function f(a){var b;if(!q&&ta(a))b=a;else{b=[];for(var c in a)a.hasOwnProperty(c)&&"$"!==c.charAt(0)&&b.push(c)}return b}var n=a.match(Sg);if(!n)throw Rg("iexp",a,ya(b));var p=n[5]||n[7],q=n[6];a=/ as /.test(n[0])&&n[1];var s=n[9];b=d(n[2]?n[1]:p);var w=a&&d(a)||b,r=s&&d(s),y=s?function(a,b){return r(c,b)}:function(a){return Ca(a)},v=function(a,b){return y(a,E(a,b))},A=d(n[2]||n[1]),t=d(n[3]||""),K=d(n[4]||""),z=d(n[8]),H={},E=q?function(a,b){H[q]=b;H[p]=
|
||||
a;return H}:function(a){H[p]=a;return H};return{trackBy:s,getTrackByValue:v,getWatchables:d(z,function(a){var b=[];a=a||[];for(var d=f(a),e=d.length,g=0;g<e;g++){var h=a===d?g:d[g],l=a[h],h=E(l,h),l=y(l,h);b.push(l);if(n[2]||n[1])l=A(c,h),b.push(l);n[4]&&(h=K(c,h),b.push(h))}return b}),getOptions:function(){for(var a=[],b={},d=z(c)||[],g=f(d),h=g.length,n=0;n<h;n++){var p=d===g?n:g[n],q=E(d[p],p),r=w(c,q),p=y(r,q),u=A(c,q),H=t(c,q),q=K(c,q),r=new e(p,r,u,H,q);a.push(r);b[p]=r}return{items:a,selectValueMap:b,
|
||||
getOptionFromViewValue:function(a){return b[v(a)]},getViewValueFromOption:function(a){return s?ca.copy(a.viewValue):a.viewValue}}}}}var e=C.document.createElement("option"),f=C.document.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","ngModel"],link:{pre:function(a,b,c,d){d[0].registerOption=A},post:function(d,h,k,l){function m(a,b){a.element=b;b.disabled=a.disabled;a.label!==b.label&&(b.label=a.label,b.textContent=a.label);a.value!==b.value&&(b.value=a.selectValue)}function n(){var a=
|
||||
t&&p.readValue();if(t)for(var b=t.items.length-1;0<=b;b--){var c=t.items[b];w(c.group)?Db(c.element.parentNode):Db(c.element)}t=K.getOptions();var d={};v&&h.prepend(B);t.items.forEach(function(a){var b;if(w(a.group)){b=d[a.group];b||(b=f.cloneNode(!1),C.appendChild(b),b.label=null===a.group?"null":a.group,d[a.group]=b);var c=e.cloneNode(!1)}else b=C,c=e.cloneNode(!1);b.appendChild(c);m(a,c)});h[0].appendChild(C);s.$render();s.$isEmpty(a)||(b=p.readValue(),(K.trackBy||y?na(a,b):a===b)||(s.$setViewValue(b),
|
||||
s.$render()))}var p=l[0],s=l[1],y=k.multiple,B;l=0;for(var r=h.children(),A=r.length;l<A;l++)if(""===r[l].value){B=r.eq(l);break}var v=!!B,z=F(e.cloneNode(!1));z.val("?");var t,K=c(k.ngOptions,h,d),C=b[0].createDocumentFragment();y?(s.$isEmpty=function(a){return!a||0===a.length},p.writeValue=function(a){t.items.forEach(function(a){a.element.selected=!1});a&&a.forEach(function(a){if(a=t.getOptionFromViewValue(a))a.element.selected=!0})},p.readValue=function(){var a=h.val()||[],b=[];q(a,function(a){(a=
|
||||
t.selectValueMap[a])&&!a.disabled&&b.push(t.getViewValueFromOption(a))});return b},K.trackBy&&d.$watchCollection(function(){if(L(s.$viewValue))return s.$viewValue.map(function(a){return K.getTrackByValue(a)})},function(){s.$render()})):(p.writeValue=function(a){var b=t.getOptionFromViewValue(a);b?(h[0].value!==b.selectValue&&(z.remove(),v||B.remove(),h[0].value=b.selectValue,b.element.selected=!0),b.element.setAttribute("selected","selected")):null===a||v?(z.remove(),v||h.prepend(B),h.val(""),B.prop("selected",
|
||||
!0),B.attr("selected",!0)):(v||B.remove(),h.prepend(z),h.val("?"),z.prop("selected",!0),z.attr("selected",!0))},p.readValue=function(){var a=t.selectValueMap[h.val()];return a&&!a.disabled?(v||B.remove(),z.remove(),t.getViewValueFromOption(a)):null},K.trackBy&&d.$watch(function(){return K.getTrackByValue(s.$viewValue)},function(){s.$render()}));v?(B.remove(),a(B)(d),B.removeClass("ng-scope")):B=F(e.cloneNode(!1));h.empty();n();d.$watchCollection(K.getWatchables,n)}}}}],Ie=["$locale","$interpolate",
|
||||
"$log",function(a,b,d){var c=/{}/g,e=/^when(Minus)?(.+)$/;return{link:function(f,g,h){function k(a){g.text(a||"")}var l=h.count,m=h.$attr.when&&g.attr(h.$attr.when),n=h.offset||0,p=f.$eval(m)||{},s={},w=b.startSymbol(),B=b.endSymbol(),r=w+l+"-"+n+B,z=ca.noop,v;q(h,function(a,b){var c=e.exec(b);c&&(c=(c[1]?"-":"")+Q(c[2]),p[c]=g.attr(h.$attr[b]))});q(p,function(a,d){s[d]=b(a.replace(c,r))});f.$watch(l,function(b){var c=parseFloat(b),e=isNaN(c);e||c in p||(c=a.pluralCat(c-n));c===v||e&&T(v)&&isNaN(v)||
|
||||
(z(),e=s[c],y(e)?(null!=b&&d.debug("ngPluralize: no rule defined for '"+c+"' in "+m),z=A,k()):z=f.$watch(e,k),v=c)})}}}],Je=["$parse","$animate","$compile",function(a,b,d){var c=N("ngRepeat"),e=function(a,b,c,d,e,m,n){a[c]=d;e&&(a[e]=m);a.$index=b;a.$first=0===b;a.$last=b===n-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(b&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,k=d.$$createComment("end ngRepeat",
|
||||
h),l=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!l)throw c("iexp",h);var m=l[1],n=l[2],p=l[3],s=l[4],l=m.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);if(!l)throw c("iidexp",m);var w=l[3]||l[1],y=l[2];if(p&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(p)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(p)))throw c("badident",p);var r,z,v,A,t={$id:Ca};s?r=a(s):(v=function(a,b){return Ca(b)},
|
||||
A=function(a){return a});return function(a,d,f,g,l){r&&(z=function(b,c,d){y&&(t[y]=b);t[w]=c;t.$index=d;return r(a,t)});var m=U();a.$watchCollection(n,function(f){var g,n,r=d[0],s,u=U(),t,C,F,E,G,D,H;p&&(a[p]=f);if(ta(f))G=f,n=z||v;else for(H in n=z||A,G=[],f)ua.call(f,H)&&"$"!==H.charAt(0)&&G.push(H);t=G.length;H=Array(t);for(g=0;g<t;g++)if(C=f===G?g:G[g],F=f[C],E=n(C,F,g),m[E])D=m[E],delete m[E],u[E]=D,H[g]=D;else{if(u[E])throw q(H,function(a){a&&a.scope&&(m[a.id]=a)}),c("dupes",h,E,F);H[g]={id:E,
|
||||
scope:void 0,clone:void 0};u[E]=!0}for(s in m){D=m[s];E=tb(D.clone);b.leave(E);if(E[0].parentNode)for(g=0,n=E.length;g<n;g++)E[g].$$NG_REMOVED=!0;D.scope.$destroy()}for(g=0;g<t;g++)if(C=f===G?g:G[g],F=f[C],D=H[g],D.scope){s=r;do s=s.nextSibling;while(s&&s.$$NG_REMOVED);D.clone[0]!=s&&b.move(tb(D.clone),null,r);r=D.clone[D.clone.length-1];e(D.scope,g,w,F,y,C,t)}else l(function(a,c){D.scope=c;var d=k.cloneNode(!1);a[a.length++]=d;b.enter(a,null,r);r=d;D.clone=a;u[D.id]=D;e(D.scope,g,w,F,y,C,t)});m=
|
||||
u})}}}}],Ke=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngShow,function(b){a[b?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],De=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngHide,function(b){a[b?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],Le=Ta(function(a,b,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&q(d,function(a,c){b.css(c,"")});a&&b.css(a)},
|
||||
!0)}),Me=["$animate","$compile",function(a,b){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(d,c,e,f){var g=[],h=[],k=[],l=[],m=function(a,b){return function(){a.splice(b,1)}};d.$watch(e.ngSwitch||e.on,function(c){var d,e;d=0;for(e=k.length;d<e;++d)a.cancel(k[d]);d=k.length=0;for(e=l.length;d<e;++d){var s=tb(h[d].clone);l[d].$destroy();(k[d]=a.leave(s)).then(m(k,d))}h.length=0;l.length=0;(g=f.cases["!"+c]||f.cases["?"])&&q(g,function(c){c.transclude(function(d,
|
||||
e){l.push(e);var f=c.element;d[d.length++]=b.$$createComment("end ngSwitchWhen");h.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],Ne=Ta({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["!"+d.ngSwitchWhen]=c.cases["!"+d.ngSwitchWhen]||[];c.cases["!"+d.ngSwitchWhen].push({transclude:e,element:b})}}),Oe=Ta({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["?"]=c.cases["?"]||[];c.cases["?"].push({transclude:e,
|
||||
element:b})}}),Tg=N("ngTransclude"),Qe=["$compile",function(a){return{restrict:"EAC",terminal:!0,compile:function(b){var d=a(b.contents());b.empty();return function(a,b,f,g,h){function k(){d(a,function(a){b.append(a)})}if(!h)throw Tg("orphan",ya(b));f.ngTransclude===f.$attr.ngTransclude&&(f.ngTransclude="");f=f.ngTransclude||f.ngTranscludeSlot;h(function(a,c){a.length?b.append(a):(k(),c.$destroy())},null,f);f&&!h.isSlotFilled(f)&&k()}}}}],qe=["$templateCache",function(a){return{restrict:"E",terminal:!0,
|
||||
compile:function(b,d){"text/ng-template"==d.type&&a.put(d.id,b[0].text)}}}],Ug={$setViewValue:A,$render:A},Vg=["$element","$scope",function(a,b){var d=this,c=new Ra;d.ngModelCtrl=Ug;d.unknownOption=F(C.document.createElement("option"));d.renderUnknownOption=function(b){b="? "+Ca(b)+" ?";d.unknownOption.val(b);a.prepend(d.unknownOption);a.val(b)};b.$on("$destroy",function(){d.renderUnknownOption=A});d.removeUnknownOption=function(){d.unknownOption.parent()&&d.unknownOption.remove()};d.readValue=function(){d.removeUnknownOption();
|
||||
return a.val()};d.writeValue=function(b){d.hasOption(b)?(d.removeUnknownOption(),a.val(b),""===b&&d.emptyOption.prop("selected",!0)):null==b&&d.emptyOption?(d.removeUnknownOption(),a.val("")):d.renderUnknownOption(b)};d.addOption=function(a,b){if(8!==b[0].nodeType){Qa(a,'"option value"');""===a&&(d.emptyOption=b);var g=c.get(a)||0;c.put(a,g+1);d.ngModelCtrl.$render();b[0].hasAttribute("selected")&&(b[0].selected=!0)}};d.removeOption=function(a){var b=c.get(a);b&&(1===b?(c.remove(a),""===a&&(d.emptyOption=
|
||||
void 0)):c.put(a,b-1))};d.hasOption=function(a){return!!c.get(a)};d.registerOption=function(a,b,c,h,k){if(h){var l;c.$observe("value",function(a){w(l)&&d.removeOption(l);l=a;d.addOption(a,b)})}else k?a.$watch(k,function(a,e){c.$set("value",a);e!==a&&d.removeOption(e);d.addOption(a,b)}):d.addOption(c.value,b);b.on("$destroy",function(){d.removeOption(c.value);d.ngModelCtrl.$render()})}}],re=function(){return{restrict:"E",require:["select","?ngModel"],controller:Vg,priority:1,link:{pre:function(a,b,
|
||||
d,c){var e=c[1];if(e){var f=c[0];f.ngModelCtrl=e;b.on("change",function(){a.$apply(function(){e.$setViewValue(f.readValue())})});if(d.multiple){f.readValue=function(){var a=[];q(b.find("option"),function(b){b.selected&&a.push(b.value)});return a};f.writeValue=function(a){var c=new Ra(a);q(b.find("option"),function(a){a.selected=w(c.get(a.value))})};var g,h=NaN;a.$watch(function(){h!==e.$viewValue||na(g,e.$viewValue)||(g=ia(e.$viewValue),e.$render());h=e.$viewValue});e.$isEmpty=function(a){return!a||
|
||||
0===a.length}}}},post:function(a,b,d,c){var e=c[1];if(e){var f=c[0];e.$render=function(){f.writeValue(e.$viewValue)}}}}}},te=["$interpolate",function(a){return{restrict:"E",priority:100,compile:function(b,d){if(w(d.value))var c=a(d.value,!0);else{var e=a(b.text(),!0);e||d.$set("value",b.text())}return function(a,b,d){var k=b.parent();(k=k.data("$selectController")||k.parent().data("$selectController"))&&k.registerOption(a,b,d,c,e)}}}}],se=ha({restrict:"E",terminal:!1}),Ic=function(){return{restrict:"A",
|
||||
require:"?ngModel",link:function(a,b,d,c){c&&(d.required=!0,c.$validators.required=function(a,b){return!d.required||!c.$isEmpty(b)},d.$observe("required",function(){c.$validate()}))}}},Hc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e,f=d.ngPattern||d.pattern;d.$observe("pattern",function(a){G(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)throw N("ngPattern")("noregexp",f,a,ya(b));e=a||void 0;c.$validate()});c.$validators.pattern=function(a,b){return c.$isEmpty(b)||
|
||||
y(e)||e.test(b)}}}}},Kc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=-1;d.$observe("maxlength",function(a){a=Z(a);e=isNaN(a)?-1:a;c.$validate()});c.$validators.maxlength=function(a,b){return 0>e||c.$isEmpty(b)||b.length<=e}}}}},Jc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=Z(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};C.angular.bootstrap?
|
||||
C.console&&console.log("WARNING: Tried to load angular more than once."):(je(),le(ca),ca.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),
|
||||
SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",
|
||||
PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a,c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),F(C.document).ready(function(){fe(C.document,Bc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
|
||||
//# sourceMappingURL=angular.min.js.map
|
||||
@@ -1,854 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, async, angular, redirectIfNeeded */
|
||||
/* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES,NOTIFICATION_TYPES */
|
||||
|
||||
// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
if (search.accessToken) {
|
||||
localStorage.token = search.accessToken;
|
||||
|
||||
// strip the accessToken and expiresAt, then preserve the rest
|
||||
delete search.accessToken;
|
||||
delete search.expiresAt;
|
||||
|
||||
// this will reload the page as this is not a hash change
|
||||
window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&'));
|
||||
}
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.multiselect']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
delay: 5000,
|
||||
startTop: 60,
|
||||
positionX: 'left',
|
||||
templateUrl: 'notification.html'
|
||||
});
|
||||
}]);
|
||||
|
||||
// configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist
|
||||
app.config(function ($sceDelegateProvider) {
|
||||
$sceDelegateProvider.resourceUrlWhitelist([
|
||||
// Allow same origin resource loads.
|
||||
'self',
|
||||
// Allow loading from our assets domain.
|
||||
'https://cloudron.io/**',
|
||||
'https://staging.cloudron.io/**',
|
||||
'https://dev.cloudron.io/**',
|
||||
// Allow local development against the appstore pages
|
||||
'http://localhost:5000/**'
|
||||
]);
|
||||
});
|
||||
|
||||
// setup all major application routes
|
||||
app.config(['$routeProvider', function ($routeProvider) {
|
||||
$routeProvider.when('/', {
|
||||
redirectTo: '/apps'
|
||||
}).when('/users', {
|
||||
controller: 'UsersController',
|
||||
templateUrl: 'views/users.html?' + window.VITE_CACHE_ID
|
||||
}).when('/user-directory', {
|
||||
controller: 'UserSettingsController',
|
||||
templateUrl: 'views/user-directory.html?' + window.VITE_CACHE_ID
|
||||
}).when('/app/:appId/:view?', {
|
||||
controller: 'AppController',
|
||||
templateUrl: 'views/app.html?' + window.VITE_CACHE_ID
|
||||
}).when('/appstore', {
|
||||
controller: 'AppStoreController',
|
||||
templateUrl: 'views/appstore.html?' + window.VITE_CACHE_ID
|
||||
}).when('/appstore/:appId', {
|
||||
controller: 'AppStoreController',
|
||||
templateUrl: 'views/appstore.html?' + window.VITE_CACHE_ID
|
||||
}).when('/apps', {
|
||||
controller: 'AppsController',
|
||||
templateUrl: 'views/apps.html?' + window.VITE_CACHE_ID
|
||||
}).when('/profile', {
|
||||
controller: 'ProfileController',
|
||||
templateUrl: 'views/profile.html?' + window.VITE_CACHE_ID
|
||||
}).when('/backups', {
|
||||
controller: 'BackupsController',
|
||||
templateUrl: 'views/backups.html?' + window.VITE_CACHE_ID
|
||||
}).when('/branding', {
|
||||
controller: 'BrandingController',
|
||||
templateUrl: 'views/branding.html?' + window.VITE_CACHE_ID
|
||||
}).when('/network', {
|
||||
controller: 'NetworkController',
|
||||
templateUrl: 'views/network.html?' + window.VITE_CACHE_ID
|
||||
}).when('/domains', {
|
||||
controller: 'DomainsController',
|
||||
templateUrl: 'views/domains.html?' + window.VITE_CACHE_ID
|
||||
}).when('/email', {
|
||||
controller: 'EmailsController',
|
||||
templateUrl: 'views/emails.html?' + window.VITE_CACHE_ID
|
||||
}).when('/emails-eventlog', {
|
||||
controller: 'EmailsEventlogController',
|
||||
templateUrl: 'views/emails-eventlog.html?' + window.VITE_CACHE_ID
|
||||
}).when('/emails-queue', {
|
||||
controller: 'EmailsQueueController',
|
||||
templateUrl: 'views/emails-queue.html?' + window.VITE_CACHE_ID
|
||||
}).when('/email/:domain/:view?', {
|
||||
controller: 'EmailController',
|
||||
templateUrl: 'views/email.html?' + window.VITE_CACHE_ID
|
||||
}).when('/notifications', {
|
||||
controller: 'NotificationsController',
|
||||
templateUrl: 'views/notifications.html?' + window.VITE_CACHE_ID
|
||||
}).when('/oidc', {
|
||||
redirectTo: '/user-directory'
|
||||
}).when('/settings', {
|
||||
controller: 'SettingsController',
|
||||
templateUrl: 'views/settings.html?' + window.VITE_CACHE_ID
|
||||
}).when('/eventlog', {
|
||||
controller: 'EventLogController',
|
||||
templateUrl: 'views/eventlog.html?' + window.VITE_CACHE_ID
|
||||
}).when('/support', {
|
||||
controller: 'SupportController',
|
||||
templateUrl: 'views/support.html?' + window.VITE_CACHE_ID
|
||||
}).when('/system', {
|
||||
controller: 'SystemController',
|
||||
templateUrl: 'views/system.html?' + window.VITE_CACHE_ID
|
||||
}).when('/services', {
|
||||
controller: 'ServicesController',
|
||||
templateUrl: 'views/services.html?' + window.VITE_CACHE_ID
|
||||
}).when('/volumes', {
|
||||
controller: 'VolumesController',
|
||||
templateUrl: 'views/volumes.html?' + window.VITE_CACHE_ID
|
||||
}).otherwise({ redirectTo: '/'});
|
||||
}]);
|
||||
|
||||
app.filter('notificationTypeToColor', function () {
|
||||
return function (n) {
|
||||
switch (n.type) {
|
||||
case NOTIFICATION_TYPES.REBOOT:
|
||||
case NOTIFICATION_TYPES.APP_OOM:
|
||||
case NOTIFICATION_TYPES.MAIL_STATUS:
|
||||
case NOTIFICATION_TYPES.CERTIFICATE_RENEWAL_FAILED:
|
||||
case NOTIFICATION_TYPES.DISK_SPACE:
|
||||
case NOTIFICATION_TYPES.BACKUP_CONFIG:
|
||||
case NOTIFICATION_TYPES.BACKUP_FAILED:
|
||||
return '#ff4c4c';
|
||||
case NOTIFICATION_TYPES.BOX_UPDATE:
|
||||
case NOTIFICATION_TYPES.MANUAL_APP_UPDATE:
|
||||
return '#f0ad4e';
|
||||
default:
|
||||
return '#2196f3';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('capitalize', function () {
|
||||
return function (s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('activeTask', function () {
|
||||
return function (app) {
|
||||
if (!app) return false;
|
||||
return app.taskId !== null;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('installSuccess', function () {
|
||||
return function (app) {
|
||||
if (!app) return false;
|
||||
return app.installationState === ISTATES.INSTALLED;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('appIsInstalledAndHealthy', function () {
|
||||
return function (app) {
|
||||
if (!app) return false;
|
||||
return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING);
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('applicationLink', function () {
|
||||
return function(app) {
|
||||
if (!app) return '';
|
||||
|
||||
// app links have http already in the fqdn
|
||||
if (app.fqdn.indexOf('http') !== 0) return 'https://' + app.fqdn;
|
||||
|
||||
return app.fqdn;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('userManagementFilter', function () {
|
||||
return function(apps, option) {
|
||||
return apps.filter(function (app) {
|
||||
if (option.id === '') return true;
|
||||
if (option.id === 'sso') return !!(app.manifest.optionalSso || app.manifest.addons.ldap || app.manifest.addons.proxyAuth);
|
||||
if (option.id === 'nosso') return app.manifest.optionalSso || (!app.manifest.addons.ldap && !app.manifest.addons.proxyAuth);
|
||||
if (option.id === 'email') return !!app.manifest.addons.email;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// this appears when an item in app grid is clicked
|
||||
app.filter('prettyAppErrorMessage', function () {
|
||||
return function (app) {
|
||||
if (!app) return '';
|
||||
|
||||
if (app.installationState === ISTATES.INSTALLED) {
|
||||
// app.health can also be null to indicate insufficient data
|
||||
if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.';
|
||||
}
|
||||
|
||||
if (app.error.reason === 'Access Denied') {
|
||||
if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
|
||||
} else if (app.error.reason === 'Already Exists') {
|
||||
if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.';
|
||||
}
|
||||
|
||||
return app.error.message;
|
||||
};
|
||||
});
|
||||
|
||||
// this appears as tool tip in app grid
|
||||
app.filter('appProgressMessage', function () {
|
||||
return function (app) {
|
||||
var message = app.message || (app.error ? app.error.message : '');
|
||||
return message;
|
||||
};
|
||||
});
|
||||
|
||||
// see apps.js $scope.states
|
||||
app.filter('selectedStateFilter', ['Client', function (Client) {
|
||||
return function selectedStateFilter(apps, selectedState) {
|
||||
return apps.filter(function (app) {
|
||||
if (!selectedState || !selectedState.state) return true;
|
||||
|
||||
if (selectedState.state === 'running') return app.runState === RSTATES.RUNNING && app.health === HSTATES.HEALTHY && app.installationState === ISTATES.INSTALLED;
|
||||
if (selectedState.state === 'stopped') return app.runState === RSTATES.STOPPED;
|
||||
if (selectedState.state === 'update_available') return !!(Client.getConfig().update[app.id] && Client.getConfig().update[app.id].manifest.version && Client.getConfig().update[app.id].manifest.version !== app.manifest.version);
|
||||
|
||||
return app.runState === RSTATES.RUNNING && (app.health !== HSTATES.HEALTHY || app.installationState !== ISTATES.INSTALLED); // not responding
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
app.filter('selectedGroupAccessFilter', function () {
|
||||
return function selectedGroupAccessFilter(apps, group) {
|
||||
return apps.filter(function (app) {
|
||||
if (!group.id) return true; // case for no filter entry
|
||||
if (!app.accessRestriction) return true;
|
||||
|
||||
if (!app.accessRestriction.groups) return false;
|
||||
|
||||
if (app.accessRestriction.groups.indexOf(group.id) !== -1) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('selectedTagFilter', function () {
|
||||
return function selectedTagFilter(apps, selectedTags) {
|
||||
return apps.filter(function (app) {
|
||||
if (selectedTags.length === 0) return true;
|
||||
if (!app.tags) return false;
|
||||
|
||||
for (var i = 0; i < selectedTags.length; i++) {
|
||||
if (app.tags.indexOf(selectedTags[i]) === -1) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('selectedDomainFilter', function () {
|
||||
return function selectedDomainFilter(apps, selectedDomain) {
|
||||
return apps.filter(function (app) {
|
||||
if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN
|
||||
if (app.type === APP_TYPES.LINK) return false;
|
||||
|
||||
if (selectedDomain.domain === app.domain) return true;
|
||||
if (app.aliasDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
||||
if (app.redirectDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('appSearchFilter', function () {
|
||||
return function appSearchFilter(apps, appSearch) {
|
||||
return apps.filter(function (app) {
|
||||
if (!appSearch) return true;
|
||||
appSearch = appSearch.toLowerCase();
|
||||
return app.fqdn.indexOf(appSearch) !== -1
|
||||
|| (app.label && app.label.toLowerCase().indexOf(appSearch) !== -1)
|
||||
|| (app.manifest.title && app.manifest.title.toLowerCase().indexOf(appSearch) !== -1)
|
||||
|| (appSearch.length >=6 && app.id.indexOf(appSearch) !== -1);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyDomains', function () {
|
||||
return function prettyDomains(domains) {
|
||||
return domains.map(function (d) { return d.domain; }).join(', ');
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('installationActive', function () {
|
||||
return function (app) {
|
||||
if (app.installationState === ISTATES.ERROR) return false;
|
||||
if (app.installationState === ISTATES.INSTALLED) return false;
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
// color indicator in app list
|
||||
app.filter('installationStateClass', function () {
|
||||
const ERROR_CLASS = 'status-error';
|
||||
const BUSY_CLASS = 'status-starting fa-beat-fade';
|
||||
const INACTIVE_CLASS = 'status-inactive';
|
||||
const ACTIVE_CLASS = 'status-active';
|
||||
|
||||
return function(app) {
|
||||
if (!app) return '';
|
||||
|
||||
switch (app.installationState) {
|
||||
case ISTATES.ERROR: return ERROR_CLASS;
|
||||
case ISTATES.INSTALLED: {
|
||||
if (app.debugMode) {
|
||||
return INACTIVE_CLASS;
|
||||
} else {
|
||||
if (app.runState === RSTATES.RUNNING) {
|
||||
if (!app.health) return BUSY_CLASS; // no data yet
|
||||
if (app.type === APP_TYPES.LINK || app.health === HSTATES.HEALTHY) return ACTIVE_CLASS;
|
||||
return ERROR_CLASS; // dead/exit/unhealthy
|
||||
} else {
|
||||
return INACTIVE_CLASS;
|
||||
}
|
||||
}
|
||||
}
|
||||
default: return BUSY_CLASS;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// this appears in the app grid
|
||||
app.filter('installationStateLabel', function () {
|
||||
return function(app) {
|
||||
if (!app) return '';
|
||||
|
||||
var waiting = app.progress === 0 ? ' (Queued)' : '';
|
||||
|
||||
switch (app.installationState) {
|
||||
case ISTATES.PENDING_INSTALL:
|
||||
return 'Installing' + waiting;
|
||||
case ISTATES.PENDING_CLONE:
|
||||
return 'Cloning' + waiting;
|
||||
case ISTATES.PENDING_LOCATION_CHANGE:
|
||||
case ISTATES.PENDING_CONFIGURE:
|
||||
case ISTATES.PENDING_RECREATE_CONTAINER:
|
||||
case ISTATES.PENDING_SERVICES_CHANGE:
|
||||
case ISTATES.PENDING_DEBUG:
|
||||
return 'Configuring' + waiting;
|
||||
case ISTATES.PENDING_RESIZE:
|
||||
return 'Resizing' + waiting;
|
||||
case ISTATES.PENDING_DATA_DIR_MIGRATION:
|
||||
return 'Migrating data' + waiting;
|
||||
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
|
||||
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
|
||||
case ISTATES.PENDING_IMPORT: return 'Importing' + waiting;
|
||||
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
|
||||
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
|
||||
case ISTATES.PENDING_START: return 'Starting' + waiting;
|
||||
case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
|
||||
case ISTATES.PENDING_RESTART: return 'Restarting' + waiting;
|
||||
case ISTATES.ERROR: {
|
||||
if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
|
||||
return 'Error';
|
||||
}
|
||||
case ISTATES.INSTALLED: {
|
||||
if (app.debugMode) {
|
||||
return 'Recovery Mode';
|
||||
} else if (app.runState === RSTATES.RUNNING) {
|
||||
if (!app.health) return 'Starting...'; // no data yet
|
||||
if (app.type === APP_TYPES.LINK) return '';
|
||||
if (app.health === HSTATES.HEALTHY) return 'Running';
|
||||
return 'Not responding'; // dead/exit/unhealthy
|
||||
} else if (app.runState === RSTATES.STOPPED) return 'Stopped';
|
||||
else return app.runState;
|
||||
}
|
||||
default: return app.installationState;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('taskName', function () {
|
||||
return function(installationState) {
|
||||
switch (installationState) {
|
||||
case ISTATES.PENDING_INSTALL: return 'install';
|
||||
case ISTATES.PENDING_CLONE: return 'clone';
|
||||
case ISTATES.PENDING_LOCATION_CHANGE: return 'location change';
|
||||
case ISTATES.PENDING_CONFIGURE: return 'configure';
|
||||
case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container';
|
||||
case ISTATES.PENDING_DEBUG: return 'debug';
|
||||
case ISTATES.PENDING_RESIZE: return 'resize';
|
||||
case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
|
||||
case ISTATES.PENDING_UNINSTALL: return 'uninstall';
|
||||
case ISTATES.PENDING_RESTORE: return 'restore';
|
||||
case ISTATES.PENDING_IMPORT: return 'import';
|
||||
case ISTATES.PENDING_UPDATE: return 'update';
|
||||
case ISTATES.PENDING_BACKUP: return 'backup';
|
||||
case ISTATES.PENDING_START: return 'start app';
|
||||
case ISTATES.PENDING_STOP: return 'stop app';
|
||||
case ISTATES.PENDING_RESTART: return 'restart app';
|
||||
default: return installationState || '';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('errorSuggestion', function () {
|
||||
return function (error) {
|
||||
if (!error) return '';
|
||||
|
||||
switch (error.reason) {
|
||||
case ERROR.ACCESS_DENIED:
|
||||
if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view';
|
||||
return '';
|
||||
case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server';
|
||||
case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server';
|
||||
case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server';
|
||||
case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running';
|
||||
case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server';
|
||||
case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server';
|
||||
case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server';
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('canUpdate', function () {
|
||||
return function (apps) {
|
||||
return apps.every(function (app) {
|
||||
return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('inProgressApps', function () {
|
||||
return function (apps) {
|
||||
return apps.filter(function (app) {
|
||||
return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyHref', function () {
|
||||
return function (input) {
|
||||
if (!input) return input;
|
||||
if (input.indexOf('http://') === 0) return input.slice('http://'.length);
|
||||
if (input.indexOf('https://') === 0) return input.slice('https://'.length);
|
||||
return input;
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyEmailAddresses', function () {
|
||||
return function prettyEmailAddresses(addresses) {
|
||||
if (!addresses) return '';
|
||||
if (addresses === '<>') return '<>';
|
||||
if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
|
||||
return addresses.slice(1, -1);
|
||||
};
|
||||
});
|
||||
|
||||
// custom directive for dynamic names in forms
|
||||
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
|
||||
app.directive('laterName', function () { // (2)
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: ['?ngModel', '^?form'], // (3)
|
||||
link: function postLink(scope, elem, attrs, ctrls) {
|
||||
attrs.$set('name', attrs.laterName);
|
||||
|
||||
var modelCtrl = ctrls[0]; // (3)
|
||||
var formCtrl = ctrls[1]; // (3)
|
||||
if (modelCtrl && formCtrl) {
|
||||
modelCtrl.$name = attrs.name; // (4)
|
||||
formCtrl.$addControl(modelCtrl); // (2)
|
||||
scope.$on('$destroy', function () {
|
||||
formCtrl.$removeControl(modelCtrl); // (5)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
|
||||
var original = $location.path;
|
||||
$location.path = function (path, reload) {
|
||||
if (reload === false) {
|
||||
var lastRoute = $route.current;
|
||||
var un = $rootScope.$on('$locationChangeSuccess', function () {
|
||||
$route.current = lastRoute;
|
||||
un();
|
||||
});
|
||||
}
|
||||
return original.apply($location, [path]);
|
||||
};
|
||||
}]);
|
||||
|
||||
app.directive('ngClickSelect', function () {
|
||||
return {
|
||||
restrict: 'AC',
|
||||
link: function (scope, element/*, attrs */) {
|
||||
element.bind('click', function () {
|
||||
var selection = window.getSelection();
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(this);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// handles various states and triggers a href or configure/repair view
|
||||
// used by attaching to controller $scope
|
||||
// if $scope.appPostInstallConfirm.show(app); exists it will be called if not yet confirmed
|
||||
function onAppClick(app, $event, isOperator, $scope) {
|
||||
function stopEvent() {
|
||||
$event.originalEvent.stopPropagation();
|
||||
$event.originalEvent.preventDefault();
|
||||
}
|
||||
|
||||
if (app.installationState !== ISTATES.INSTALLED) {
|
||||
if (app.installationState === ISTATES.ERROR && isOperator) $scope.showAppConfigure(app, 'repair');
|
||||
return stopEvent();
|
||||
}
|
||||
|
||||
// app.health can also be null to indicate insufficient data
|
||||
if (!app.health) return stopEvent();
|
||||
if (app.runState === RSTATES.STOPPED) return stopEvent();
|
||||
if (app.runState === RSTATES.STOPPED) return stopEvent();
|
||||
|
||||
if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) {
|
||||
if (isOperator) $scope.showAppConfigure(app, 'repair');
|
||||
return stopEvent();
|
||||
}
|
||||
|
||||
if (app.pendingPostInstallConfirmation && $scope.appPostInstallConfirm) {
|
||||
$scope.appPostInstallConfirm.show(app);
|
||||
return stopEvent();
|
||||
}
|
||||
}
|
||||
|
||||
app.directive('ngClickReveal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.addClass('hand');
|
||||
|
||||
var value = '';
|
||||
|
||||
scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
element.html('<i>hidden</i>');
|
||||
value = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
element.bind('click', function () {
|
||||
element.text(value);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// https://codepen.io/webmatze/pen/isuHh
|
||||
app.directive('tagInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
inputTags: '=taglist'
|
||||
},
|
||||
require: '^form',
|
||||
link: function ($scope, element, attrs, formCtrl) {
|
||||
$scope.defaultWidth = 200;
|
||||
$scope.tagText = ''; // current tag being edited
|
||||
$scope.placeholder = attrs.placeholder;
|
||||
$scope.tagArray = function () {
|
||||
if ($scope.inputTags === undefined) {
|
||||
return [];
|
||||
}
|
||||
return $scope.inputTags.split(' ').filter(function (tag) {
|
||||
return tag !== '';
|
||||
});
|
||||
};
|
||||
$scope.addTag = function () {
|
||||
var tagArray = $scope.tagArray();
|
||||
|
||||
// prevent adding empty or existing items
|
||||
if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) {
|
||||
return $scope.tagText = '';
|
||||
}
|
||||
|
||||
tagArray.push($scope.tagText);
|
||||
$scope.inputTags = tagArray.join(' ');
|
||||
return $scope.tagText = '';
|
||||
};
|
||||
$scope.deleteTag = function (key) {
|
||||
var tagArray;
|
||||
tagArray = $scope.tagArray();
|
||||
if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) {
|
||||
tagArray.pop();
|
||||
} else {
|
||||
if (key !== undefined) {
|
||||
tagArray.splice(key, 1);
|
||||
}
|
||||
}
|
||||
formCtrl.$setDirty();
|
||||
return $scope.inputTags = tagArray.join(' ');
|
||||
};
|
||||
$scope.$watch('tagText', function (newVal, oldVal) {
|
||||
var tempEl;
|
||||
if (!(newVal === oldVal && newVal === undefined)) {
|
||||
tempEl = $('<span>' + newVal + '</span>').appendTo('body');
|
||||
$scope.inputWidth = tempEl.width() + 5;
|
||||
if ($scope.inputWidth < $scope.defaultWidth) {
|
||||
$scope.inputWidth = $scope.defaultWidth;
|
||||
}
|
||||
return tempEl.remove();
|
||||
}
|
||||
});
|
||||
element.bind('click', function () {
|
||||
element[0].firstChild.lastChild.focus();
|
||||
});
|
||||
element.bind('keydown', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (key === 8) {
|
||||
return $scope.$apply('deleteTag()');
|
||||
}
|
||||
});
|
||||
element.bind('keyup', function (e) {
|
||||
var key = e.which;
|
||||
if (key === 9 || key === 13 || key === 32) {
|
||||
e.preventDefault();
|
||||
return $scope.$apply('addTag()');
|
||||
}
|
||||
});
|
||||
},
|
||||
template:
|
||||
'<div class="tag-input-container">' +
|
||||
'<div class="btn-group input-tag" data-ng-repeat="tag in tagArray()">' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" disabled>{{ tag }}</button>' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" data-ng-click="deleteTag($index)">×</button>' +
|
||||
'</div>' +
|
||||
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
|
||||
fitTextConfigProvider.config = {
|
||||
loadDelay: 250,
|
||||
compressor: 0.9,
|
||||
min: 8,
|
||||
max: 24
|
||||
};
|
||||
}]);
|
||||
|
||||
app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) {
|
||||
$scope.initialized = false; // used to animate the UI
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
$scope.subscription = {};
|
||||
$scope.notificationCount = 0;
|
||||
$scope.hideNavBarActions = $location.path() === '/logs';
|
||||
$scope.backgroundImageUrl = '';
|
||||
|
||||
$scope.closeNavbar = function () {
|
||||
$('.navbar-collapse').collapse('hide');
|
||||
};
|
||||
|
||||
$scope.reboot = {
|
||||
busy: false,
|
||||
|
||||
show: function () {
|
||||
$scope.reboot.busy = false;
|
||||
$('#rebootModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.reboot.busy = true;
|
||||
|
||||
Client.reboot(function (error) {
|
||||
if (error) return Client.error(error);
|
||||
|
||||
$('#rebootModal').modal('hide');
|
||||
|
||||
// trigger refetch to show offline banner
|
||||
$timeout(function () { Client.getStatus(function () {}); }, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isActive = function (url) {
|
||||
if (!$route.current) return false;
|
||||
return $route.current.$$route.originalPath.indexOf(url) === 0;
|
||||
};
|
||||
|
||||
$scope.logout = function (event) {
|
||||
event.stopPropagation();
|
||||
$scope.initialized = false;
|
||||
Client.logout();
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.subscription);
|
||||
};
|
||||
|
||||
// NOTE: this function is exported and called from the appstore.js
|
||||
$scope.updateSubscriptionStatus = function () {
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
});
|
||||
};
|
||||
|
||||
function refreshNotifications() {
|
||||
if (!Client.getUserInfo().isAtLeastAdmin) return;
|
||||
|
||||
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
|
||||
if (error) console.error(error);
|
||||
else $scope.notificationCount = results.length;
|
||||
});
|
||||
}
|
||||
|
||||
// update state of acknowledged notification
|
||||
$scope.notificationAcknowledged = function () {
|
||||
refreshNotifications();
|
||||
};
|
||||
|
||||
function redirectOnMandatory2FA() {
|
||||
if (Client.getConfig().mandatory2FA) {
|
||||
if (Client.getUserInfo().twoFactorAuthenticationEnabled) return; // user already has 2fa
|
||||
if (Client.getUserInfo().source && $scope.config.external2FA) return; // 2fa is external
|
||||
|
||||
$location.path('/profile').search({ setup2fa: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force
|
||||
$scope.$on('$routeChangeStart', function (/* event */) {
|
||||
if ($scope.initialized) redirectOnMandatory2FA();
|
||||
});
|
||||
|
||||
var gPlatformStatusNotification = null;
|
||||
function trackPlatformStatus() {
|
||||
Client.getPlatformStatus(function (error, result) {
|
||||
if (error) return console.error('Failed to get platform status.', error);
|
||||
|
||||
// see box/src/platform.js
|
||||
if (result.message === 'Ready') {
|
||||
if (gPlatformStatusNotification) {
|
||||
gPlatformStatusNotification.kill();
|
||||
gPlatformStatusNotification = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gPlatformStatusNotification) {
|
||||
var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false };
|
||||
|
||||
Notification.primary(options).then(function (result) {
|
||||
gPlatformStatusNotification = result;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
});
|
||||
} else {
|
||||
gPlatformStatusNotification.message = result.message;
|
||||
$timeout(trackPlatformStatus, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// this loads the very first thing when accessing via IP or domain
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status, 'dashboard')) return; // we got redirected...
|
||||
|
||||
// check version and force reload if needed
|
||||
if (!localStorage.version) {
|
||||
localStorage.version = status.version;
|
||||
} else if (localStorage.version !== status.version) {
|
||||
localStorage.version = status.version;
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
console.log('Running dashboard version ', localStorage.version);
|
||||
|
||||
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
||||
async.series([
|
||||
Client.refreshProfile.bind(Client),
|
||||
Client.refreshConfig.bind(Client),
|
||||
Client.refreshAvailableLanguages.bind(Client),
|
||||
Client.refreshInstalledApps.bind(Client)
|
||||
], function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
if (Client.getUserInfo().hasBackgroundImage) {
|
||||
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
|
||||
document.getElementById('mainContentContainer').classList.add('has-background');
|
||||
}
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
redirectOnMandatory2FA();
|
||||
|
||||
$interval(refreshNotifications, 60 * 1000);
|
||||
refreshNotifications();
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error && error.statusCode === 412) return; // not yet registered
|
||||
if (error && error.statusCode === 402) return; // invalid appstore token
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.subscription = subscription;
|
||||
|
||||
// only track platform status if we are registered
|
||||
trackPlatformStatus();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onConfig(function (config) {
|
||||
if (config.cloudronName) {
|
||||
document.title = config.cloudronName;
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['updateModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
@@ -1,160 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, $, showdown */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']);
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true,
|
||||
openLinksInNewWindow: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
return converter.makeHtml(text);
|
||||
};
|
||||
});
|
||||
|
||||
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
|
||||
app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.config(['$translateProvider', function ($translateProvider) {
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'translation/',
|
||||
suffix: '.json'
|
||||
});
|
||||
$translateProvider.preferredLanguage('en');
|
||||
$translateProvider.fallbackLanguage('en');
|
||||
}]);
|
||||
|
||||
// Add shorthand "tr" filter to avoid having ot use "translate"
|
||||
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
|
||||
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
|
||||
function translateFilterFactory($parse, $translate) {
|
||||
var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
|
||||
if (!angular.isObject(interpolateParams)) {
|
||||
var ctx = this || {
|
||||
'__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
|
||||
};
|
||||
interpolateParams = $parse(interpolateParams)(ctx);
|
||||
}
|
||||
|
||||
return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
|
||||
};
|
||||
|
||||
if ($translate.statefulFilter()) {
|
||||
translateFilter.$stateful = true;
|
||||
}
|
||||
|
||||
return translateFilter;
|
||||
}
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
|
||||
app.controller('PasswordResetController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
const search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.mode = '';
|
||||
$scope.busy = false;
|
||||
$scope.error = false;
|
||||
$scope.branding = null;
|
||||
$scope.username = '';
|
||||
$scope.password = '';
|
||||
$scope.totpToken = '';
|
||||
$scope.passwordResetIdentifier = '';
|
||||
$scope.newPassword = '';
|
||||
$scope.newPasswordRepeat = '';
|
||||
|
||||
const API_ORIGIN = window.cloudronApiOrigin || window.location.origin;
|
||||
|
||||
$scope.onPasswordReset = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
var data = {
|
||||
identifier: $scope.passwordResetIdentifier
|
||||
};
|
||||
|
||||
function done(error) {
|
||||
if (error) $scope.error = error.message;
|
||||
$scope.busy = false;
|
||||
$scope.mode = 'passwordResetDone';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset_request', data).success(done).error(done);
|
||||
};
|
||||
|
||||
$scope.onNewPassword = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
var data = {
|
||||
resetToken: search.resetToken,
|
||||
password: $scope.newPassword,
|
||||
totpToken: $scope.totpToken
|
||||
};
|
||||
|
||||
function error(data, status) {
|
||||
console.log('error', status);
|
||||
$scope.busy = false;
|
||||
|
||||
if (status === 401) $scope.error = data.message;
|
||||
else if (status === 409) $scope.error = 'Ask your admin for an invite link first';
|
||||
else $scope.error = 'Unknown error';
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/password_reset', data).success(function (data, status) {
|
||||
if (status !== 202) return error(data, status);
|
||||
|
||||
// set token to autologin
|
||||
localStorage.token = data.accessToken;
|
||||
|
||||
$scope.mode = 'newPasswordDone';
|
||||
}).error(function (data, status) {
|
||||
error(data, status);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showPasswordReset = function () {
|
||||
window.document.title = 'Password Reset Request';
|
||||
$scope.mode = 'passwordReset';
|
||||
$scope.passwordResetIdentifier = '';
|
||||
setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200);
|
||||
};
|
||||
|
||||
$scope.showNewPassword = function () {
|
||||
window.document.title = 'Set New Password';
|
||||
$scope.mode = 'newPassword';
|
||||
setTimeout(function () { $('#inputNewPassword').focus(); }, 200);
|
||||
};
|
||||
|
||||
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.branding = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
|
||||
// Init into the correct view
|
||||
if (search.passwordReset) {
|
||||
$scope.showPasswordReset();
|
||||
} else if (search.resetToken) {
|
||||
$scope.showNewPassword();
|
||||
} else if (search.accessToken || search.access_token) { // auto-login feature
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
$scope.showPasswordReset();
|
||||
}
|
||||
}]);
|
||||
@@ -1,397 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, redirectIfNeeded */
|
||||
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO, REGIONS_HETZNER */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.controller('RestoreController', ['$scope', 'Client', function ($scope, Client) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.busy = false;
|
||||
$scope.error = {};
|
||||
$scope.message = ''; // progress
|
||||
|
||||
// variables here have to match the import config logic!
|
||||
$scope.provider = '';
|
||||
$scope.bucket = '';
|
||||
$scope.prefix = '';
|
||||
$scope.mountPoint = '';
|
||||
$scope.accessKeyId = '';
|
||||
$scope.secretAccessKey = '';
|
||||
$scope.gcsKey = { keyFileName: '', content: '' };
|
||||
$scope.region = '';
|
||||
$scope.endpoint = '';
|
||||
$scope.backupFolder = '';
|
||||
$scope.remotePath = '';
|
||||
$scope.instanceId = '';
|
||||
$scope.acceptSelfSignedCerts = false;
|
||||
$scope.format = 'tgz';
|
||||
$scope.advancedVisible = false;
|
||||
$scope.password = '';
|
||||
$scope.encryptedFilenames = true;
|
||||
$scope.encrypted = false; // only used if a backup config contains that flag
|
||||
$scope.setupToken = '';
|
||||
$scope.skipDnsSetup = false;
|
||||
$scope.disk = null;
|
||||
$scope.blockDevices = [];
|
||||
|
||||
$scope.mountOptions = {
|
||||
host: '',
|
||||
remoteDir: '',
|
||||
username: '',
|
||||
password: '',
|
||||
diskPath: '',
|
||||
user: '',
|
||||
seal: true,
|
||||
port: 22,
|
||||
privateKey: ''
|
||||
};
|
||||
|
||||
$scope.$watch('disk', function (newValue) {
|
||||
if (!newValue) return;
|
||||
$scope.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
|
||||
});
|
||||
|
||||
$scope.ipv4Config = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.ipv6Config = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.ipProviders = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.s3Regions = REGIONS_S3;
|
||||
$scope.wasabiRegions = REGIONS_WASABI;
|
||||
$scope.doSpacesRegions = REGIONS_DIGITALOCEAN;
|
||||
$scope.hetznerRegions = REGIONS_HETZNER;
|
||||
$scope.exoscaleSosRegions = REGIONS_EXOSCALE;
|
||||
$scope.scalewayRegions = REGIONS_SCALEWAY;
|
||||
$scope.linodeRegions = REGIONS_LINODE;
|
||||
$scope.ovhRegions = REGIONS_OVH;
|
||||
$scope.ionosRegions = REGIONS_IONOS;
|
||||
$scope.upcloudRegions = REGIONS_UPCLOUD;
|
||||
$scope.vultrRegions = REGIONS_VULTR;
|
||||
$scope.contaboRegions = REGIONS_CONTABO;
|
||||
|
||||
$scope.storageProviders = STORAGE_PROVIDERS;
|
||||
|
||||
$scope.formats = BACKUP_FORMATS;
|
||||
|
||||
$scope.s3like = function (provider) {
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos'
|
||||
|| provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage' || provider === 'hetzner-objectstorage'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
||||
|| provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
||||
|| provider === 'contabo-objectstorage';
|
||||
};
|
||||
|
||||
$scope.mountlike = function (provider) {
|
||||
return provider === 'disk' || provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
|
||||
};
|
||||
|
||||
$scope.restore = function () {
|
||||
$scope.error = {};
|
||||
$scope.busy = true;
|
||||
|
||||
var backupConfig = {
|
||||
provider: $scope.provider,
|
||||
format: $scope.format,
|
||||
};
|
||||
if ($scope.password) {
|
||||
backupConfig.password = $scope.password;
|
||||
backupConfig.encryptedFilenames = $scope.encryptedFilenames;
|
||||
}
|
||||
|
||||
// only set provider specific fields, this will clear them in the db
|
||||
if ($scope.s3like(backupConfig.provider)) {
|
||||
backupConfig.bucket = $scope.bucket;
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
backupConfig.accessKeyId = $scope.accessKeyId;
|
||||
backupConfig.secretAccessKey = $scope.secretAccessKey;
|
||||
|
||||
if ($scope.endpoint) backupConfig.endpoint = $scope.endpoint;
|
||||
|
||||
if (backupConfig.provider === 's3') {
|
||||
if ($scope.region) backupConfig.region = $scope.region;
|
||||
delete backupConfig.endpoint;
|
||||
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
|
||||
backupConfig.region = backupConfig.region || 'us-east-1';
|
||||
backupConfig.acceptSelfSignedCerts = $scope.acceptSelfSignedCerts;
|
||||
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
|
||||
} else if (backupConfig.provider === 'exoscale-sos') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'wasabi') {
|
||||
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'scaleway-objectstorage') {
|
||||
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'linode-objectstorage') {
|
||||
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ovh-objectstorage') {
|
||||
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
||||
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
||||
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'contabo-objectstorage') {
|
||||
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
|
||||
} else if (backupConfig.provider === 'upcloud-objectstorage') {
|
||||
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
||||
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
} else if (backupConfig.provider === 'hetzner-objectstorage') {
|
||||
backupConfig.region = 'us-east-1';
|
||||
backupConfig.signatureVersion = 'v4';
|
||||
}
|
||||
} else if (backupConfig.provider === 'gcs') {
|
||||
backupConfig.bucket = $scope.bucket;
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.gcsKey.content);
|
||||
backupConfig.projectId = serviceAccountKey.project_id;
|
||||
backupConfig.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) {
|
||||
throw 'fields_missing';
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.error.gcsKeyInput = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if ($scope.mountlike(backupConfig.provider)) {
|
||||
backupConfig.prefix = $scope.prefix;
|
||||
backupConfig.mountOptions = {};
|
||||
|
||||
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
|
||||
backupConfig.mountOptions.host = $scope.mountOptions.host;
|
||||
backupConfig.mountOptions.remoteDir = $scope.mountOptions.remoteDir;
|
||||
|
||||
if (backupConfig.provider === 'cifs') {
|
||||
backupConfig.mountOptions.username = $scope.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.mountOptions.password;
|
||||
backupConfig.mountOptions.seal = $scope.mountOptions.seal;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey;
|
||||
}
|
||||
} else if (backupConfig.provider === 'disk' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
|
||||
backupConfig.mountOptions.diskPath = $scope.mountOptions.diskPath;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.mountPoint;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.backupFolder;
|
||||
}
|
||||
|
||||
if ($scope.remotePath.indexOf('/') === -1) {
|
||||
$scope.error.generic = 'Backup id must include the directory path';
|
||||
$scope.error.remotePath = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.remotePath.indexOf('box') === -1) {
|
||||
$scope.error.generic = 'Backup id must contain "box"';
|
||||
$scope.error.remotePath = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var version = $scope.remotePath.match(/_v(\d+.\d+.\d+)/);
|
||||
if (!version) {
|
||||
$scope.error.generic = 'Backup id is missing version information';
|
||||
$scope.error.remotePath = true;
|
||||
$scope.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
backupConfig: backupConfig,
|
||||
remotePath: $scope.remotePath.replace(/\.tar\.gz(\.enc)?$/, ''),
|
||||
version: version ? version[1] : '',
|
||||
ipv4Config: $scope.ipv4Config,
|
||||
ipv6Config: $scope.ipv6Config,
|
||||
skipDnsSetup: $scope.skipDnsSetup,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.restore(data, function (error) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 424) {
|
||||
$scope.error.generic = error.message;
|
||||
|
||||
if (error.message.indexOf('AWS Access Key Id') !== -1) {
|
||||
$scope.error.accessKeyId = true;
|
||||
$scope.accessKeyId = '';
|
||||
$scope.configureBackupForm.accessKeyId.$setPristine();
|
||||
$('#inputConfigureBackupAccessKeyId').focus();
|
||||
} else if (error.message.indexOf('not match the signature') !== -1 ) {
|
||||
$scope.error.secretAccessKey = true;
|
||||
$scope.secretAccessKey = '';
|
||||
$scope.configureBackupForm.secretAccessKey.$setPristine();
|
||||
$('#inputConfigureBackupSecretAccessKey').focus();
|
||||
} else if (error.message.toLowerCase() === 'access denied') {
|
||||
$scope.error.bucket = true;
|
||||
$scope.bucket = '';
|
||||
$scope.configureBackupForm.bucket.$setPristine();
|
||||
$('#inputConfigureBackupBucket').focus();
|
||||
} else if (error.message.indexOf('ECONNREFUSED') !== -1) {
|
||||
$scope.error.generic = 'Unknown region';
|
||||
$scope.error.region = true;
|
||||
$scope.configureBackupForm.region.$setPristine();
|
||||
$('#inputConfigureBackupDORegion').focus();
|
||||
} else if (error.message.toLowerCase() === 'wrong region') {
|
||||
$scope.error.generic = 'Wrong S3 Region';
|
||||
$scope.error.region = true;
|
||||
$scope.configureBackupForm.region.$setPristine();
|
||||
$('#inputConfigureBackupS3Region').focus();
|
||||
} else {
|
||||
$('#inputConfigureBackupBucket').focus();
|
||||
}
|
||||
} else {
|
||||
$scope.error.generic = error.message;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
waitForRestore();
|
||||
});
|
||||
};
|
||||
|
||||
function waitForRestore() {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (!error && !status.restore.active) { // restore finished
|
||||
if (status.restore.errorMessage) {
|
||||
$scope.busy = false;
|
||||
$scope.error.generic = status.restore.errorMessage;
|
||||
} else { // restore worked, redirect to admin page
|
||||
window.location.href = 'https://' + status.adminFqdn + '/';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!error) $scope.message = status.restore.message;
|
||||
|
||||
setTimeout(waitForRestore, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.gcsKey, 'content', 'keyFileName');
|
||||
|
||||
document.getElementById('backupConfigFileInput').onchange = function (event) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read backup config');
|
||||
|
||||
var backupConfig;
|
||||
try {
|
||||
backupConfig = JSON.parse(result.target.result);
|
||||
} catch (e) {
|
||||
console.error('Unable to parse backup config');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.$apply(function () {
|
||||
// we assume property names match here, this does not yet work for gcs keys
|
||||
Object.keys(backupConfig).forEach(function (k) {
|
||||
if (k in $scope) $scope[k] = backupConfig[k];
|
||||
});
|
||||
|
||||
// this allows the config to potentially have a raw password (though our UI sets it to placeholder)
|
||||
if ($scope.mountOptions.password === SECRET_PLACEHOLDER) $scope.mountOptions.password = '';
|
||||
});
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
};
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status, 'restore')) return; // redirected to some other view...
|
||||
|
||||
if (status.restore.active) return waitForRestore();
|
||||
|
||||
if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage; // any previous restore error
|
||||
|
||||
Client.getProvisionBlockDevices(function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to list blockdevices:', error);
|
||||
} else {
|
||||
// only offer non /, /boot or /home disks
|
||||
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
|
||||
// only offer xfs and ext4 disks
|
||||
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
|
||||
|
||||
// amend label for UI
|
||||
result.forEach(function (d) { d.label = d.path; });
|
||||
}
|
||||
|
||||
$scope.blockDevices = result;
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
|
||||
Client.detectIp(function (error, ip) { // this is never supposed to error
|
||||
if (!error) $scope.ipv4Config.provider = ip.ipv4 ? 'generic' : 'noop';
|
||||
if (!error) $scope.ipv6Config.provider = ip.ipv6 ? 'generic' : 'noop';
|
||||
|
||||
$scope.initialized = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}]);
|
||||
@@ -1,344 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $, angular, Clipboard, ENDPOINTS_OVH, redirectIfNeeded */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
|
||||
app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', function ($scope, $http, $timeout, Client) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.state = null; // 'initialized', 'waitingForDnsSetup', 'waitingForBox'
|
||||
$scope.error = {};
|
||||
$scope.provider = '';
|
||||
$scope.showDNSSetup = false;
|
||||
$scope.instanceId = '';
|
||||
$scope.advancedVisible = false;
|
||||
$scope.clipboardDone = false;
|
||||
$scope.search = window.location.search;
|
||||
$scope.setupToken = '';
|
||||
$scope.taskMinutesActive = null;
|
||||
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
|
||||
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
|
||||
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
|
||||
{ name: 'Self-Signed', value: 'fallback' }, // this is not 'Custom' because we don't allow user to upload certs during setup phase
|
||||
];
|
||||
|
||||
$scope.ipv4Config = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.ipv6Config = {
|
||||
provider: 'generic',
|
||||
ip: '',
|
||||
ifname: ''
|
||||
};
|
||||
|
||||
$scope.ipProviders = [
|
||||
{ name: 'Disabled', value: 'noop' },
|
||||
{ name: 'Public IP', value: 'generic' },
|
||||
{ name: 'Static IP Address', value: 'fixed' },
|
||||
{ name: 'Network Interface', value: 'network-interface' }
|
||||
];
|
||||
|
||||
$scope.ovhEndpoints = ENDPOINTS_OVH;
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
};
|
||||
|
||||
// If we migrate the api origin we have to poll the new location
|
||||
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
|
||||
|
||||
// keep in sync with domains.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'deSEC', value: 'desec' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'DNSimple', value: 'dnsimple' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'INWX', value: 'inwx' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'OVH', value: 'ovh' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
$scope.dnsCredentials = {
|
||||
busy: false,
|
||||
domain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
gandiApiKey: '',
|
||||
gandiTokenType: 'PAT',
|
||||
cloudflareEmail: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
cloudflareDefaultProxyStatus: false,
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
dnsimpleAccessToken: '',
|
||||
hetznerToken: '',
|
||||
inwxUsername: '',
|
||||
inwxPassword: '',
|
||||
vultrToken: '',
|
||||
deSecToken: '',
|
||||
nameComUsername: '',
|
||||
nameComToken: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
ovhEndpoint: 'ovh-eu',
|
||||
ovhConsumerKey: '',
|
||||
ovhAppKey: '',
|
||||
ovhAppSecret: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setDefaultTlsProvider = function () {
|
||||
var dnsProvider = $scope.dnsCredentials.provider;
|
||||
// wildcard LE won't work without automated DNS
|
||||
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod';
|
||||
} else {
|
||||
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod-wildcard';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName');
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.error = {};
|
||||
|
||||
var provider = $scope.dnsCredentials.provider;
|
||||
|
||||
var config = {};
|
||||
|
||||
if (provider === 'route53') {
|
||||
config.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
config.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
} else if (provider === 'gcdns') {
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content);
|
||||
config.projectId = serviceAccountKey.project_id;
|
||||
config.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!config.projectId || !config.credentials || !config.credentials.client_email || !config.credentials.private_key) {
|
||||
throw new Error('One or more fields are missing in the JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.error.dnsCredentials = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.dnsCredentials.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
config.token = $scope.dnsCredentials.digitalOceanToken;
|
||||
} else if (provider === 'gandi') {
|
||||
config.token = $scope.dnsCredentials.gandiApiKey;
|
||||
config.tokenType = $scope.dnsCredentials.gandiTokenType;
|
||||
} else if (provider === 'godaddy') {
|
||||
config.apiKey = $scope.dnsCredentials.godaddyApiKey;
|
||||
config.apiSecret = $scope.dnsCredentials.godaddyApiSecret;
|
||||
} else if (provider === 'cloudflare') {
|
||||
config.email = $scope.dnsCredentials.cloudflareEmail;
|
||||
config.token = $scope.dnsCredentials.cloudflareToken;
|
||||
config.tokenType = $scope.dnsCredentials.cloudflareTokenType;
|
||||
config.defaultProxyStatus = $scope.dnsCredentials.cloudflareDefaultProxyStatus;
|
||||
} else if (provider === 'linode') {
|
||||
config.token = $scope.dnsCredentials.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
config.accessKey = $scope.dnsCredentials.bunnyAccessKey;
|
||||
} else if (provider === 'dnsimple') {
|
||||
config.accessToken = $scope.dnsCredentials.dnsimpleAccessToken;
|
||||
} else if (provider === 'hetzner') {
|
||||
config.token = $scope.dnsCredentials.hetznerToken;
|
||||
} else if (provider === 'inwx') {
|
||||
config.username = $scope.dnsCredentials.inwxUsername;
|
||||
config.password = $scope.dnsCredentials.inwxPassword;
|
||||
} else if (provider === 'vultr') {
|
||||
config.token = $scope.dnsCredentials.vultrToken;
|
||||
} else if (provider === 'desec') {
|
||||
config.token = $scope.dnsCredentials.deSecToken;
|
||||
} else if (provider === 'namecom') {
|
||||
config.username = $scope.dnsCredentials.nameComUsername;
|
||||
config.token = $scope.dnsCredentials.nameComToken;
|
||||
} else if (provider === 'namecheap') {
|
||||
config.token = $scope.dnsCredentials.namecheapApiKey;
|
||||
config.username = $scope.dnsCredentials.namecheapUsername;
|
||||
} else if (provider === 'netcup') {
|
||||
config.customerNumber = $scope.dnsCredentials.netcupCustomerNumber;
|
||||
config.apiKey = $scope.dnsCredentials.netcupApiKey;
|
||||
config.apiPassword = $scope.dnsCredentials.netcupApiPassword;
|
||||
} else if (provider === 'ovh') {
|
||||
config.endpoint = $scope.dnsCredentials.ovhEndpoint;
|
||||
config.consumerKey = $scope.dnsCredentials.ovhConsumerKey;
|
||||
config.appKey = $scope.dnsCredentials.ovhAppKey;
|
||||
config.appSecret = $scope.dnsCredentials.ovhAppSecret;
|
||||
} else if (provider === 'porkbun') {
|
||||
config.apikey = $scope.dnsCredentials.porkbunApikey;
|
||||
config.secretapikey = $scope.dnsCredentials.porkbunSecretapikey;
|
||||
}
|
||||
|
||||
var tlsConfig = {
|
||||
provider: $scope.dnsCredentials.tlsConfig.provider,
|
||||
wildcard: false
|
||||
};
|
||||
if ($scope.dnsCredentials.tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
||||
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
||||
tlsConfig.wildcard = true;
|
||||
}
|
||||
|
||||
var data = {
|
||||
domainConfig: {
|
||||
domain: $scope.dnsCredentials.domain,
|
||||
zoneName: $scope.dnsCredentials.zoneName,
|
||||
provider: provider,
|
||||
config: config,
|
||||
tlsConfig: tlsConfig
|
||||
},
|
||||
ipv4Config: $scope.ipv4Config,
|
||||
ipv6Config: $scope.ipv6Config,
|
||||
providerToken: $scope.instanceId,
|
||||
setupToken: $scope.setupToken
|
||||
};
|
||||
|
||||
Client.setup(data, function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
if (error.statusCode === 422) {
|
||||
if (provider === 'ami') {
|
||||
$scope.error.ami = error.message;
|
||||
} else {
|
||||
$scope.error.setup = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.error.dnsCredentials = error.message;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
waitForDnsSetup();
|
||||
});
|
||||
};
|
||||
|
||||
function waitForDnsSetup() {
|
||||
$scope.state = 'waitingForDnsSetup';
|
||||
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
if (!error && !status.setup.active) {
|
||||
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
|
||||
$scope.error.setup = status.setup.errorMessage;
|
||||
$scope.state = 'initialized';
|
||||
$scope.dnsCredentials.busy = false;
|
||||
} else { // proceed to activation
|
||||
window.location.href = 'https://' + status.adminFqdn + '/activation.html' + (window.location.search);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
$scope.message = status.setup.message;
|
||||
$scope.taskMinutesActive = (new Date() - new Date(status.setup.startTime)) / 60000;
|
||||
}
|
||||
|
||||
setTimeout(waitForDnsSetup, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getProvisionStatus(function (error, status) {
|
||||
$scope.state = 'waitingForBox';
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
if (redirectIfNeeded(status, 'setup')) return; // redirected to some other view...
|
||||
|
||||
if (status.setup.active) return waitForDnsSetup();
|
||||
|
||||
$scope.error.setup = status.setup.errorMessage; // show any previous error
|
||||
|
||||
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') {
|
||||
$scope.dnsCredentials.provider = 'digitalocean';
|
||||
} else if (status.provider === 'linode' || status.provider === 'linode-oneclick' || status.provider === 'linode-stackscript') {
|
||||
$scope.dnsCredentials.provider = 'linode';
|
||||
} else if (status.provider === 'vultr' || status.provider === 'vultr-mp') {
|
||||
$scope.dnsCredentials.provider = 'vultr';
|
||||
} else if (status.provider === 'gce') {
|
||||
$scope.dnsCredentials.provider = 'gcdns';
|
||||
} else if (status.provider === 'ami') {
|
||||
// aws marketplace made a policy change that they one cannot provide route53 IAM credentials
|
||||
$scope.dnsCredentials.provider = 'wildcard';
|
||||
}
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.setupToken = search.setupToken;
|
||||
$scope.provider = status.provider;
|
||||
|
||||
Client.detectIp(function (error, ip) { // this is never supposed to error
|
||||
if (!error) $scope.ipv4Config.provider = ip.ipv4 ? 'generic' : 'noop';
|
||||
if (!error) $scope.ipv6Config.provider = ip.ipv6 ? 'generic' : 'noop';
|
||||
|
||||
$scope.state = 'initialized';
|
||||
|
||||
setTimeout(function () { $("[autofocus]:first").focus(); }, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var clipboard = new Clipboard('.clipboard');
|
||||
clipboard.on('success', function () {
|
||||
$scope.$apply(function () { $scope.clipboardDone = true; });
|
||||
$timeout(function () { $scope.clipboardDone = false; }, 5000);
|
||||
});
|
||||
|
||||
init();
|
||||
}]);
|
||||
@@ -1,151 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular, $, showdown */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']);
|
||||
|
||||
app.filter('markdown2html', function () {
|
||||
var converter = new showdown.Converter({
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
tables: true,
|
||||
openLinksInNewWindow: true
|
||||
});
|
||||
|
||||
return function (text) {
|
||||
return converter.makeHtml(text);
|
||||
};
|
||||
});
|
||||
|
||||
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
|
||||
app.config(function ($sceProvider) {
|
||||
$sceProvider.enabled(false);
|
||||
});
|
||||
|
||||
app.config(['$translateProvider', function ($translateProvider) {
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'translation/',
|
||||
suffix: '.json'
|
||||
});
|
||||
$translateProvider.useLocalStorage();
|
||||
$translateProvider.preferredLanguage('en');
|
||||
$translateProvider.fallbackLanguage('en');
|
||||
}]);
|
||||
|
||||
// Add shorthand "tr" filter to avoid having ot use "translate"
|
||||
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
|
||||
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
|
||||
function translateFilterFactory($parse, $translate) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
|
||||
if (!angular.isObject(interpolateParams)) {
|
||||
var ctx = this || {
|
||||
'__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
|
||||
};
|
||||
interpolateParams = $parse(interpolateParams)(ctx);
|
||||
}
|
||||
|
||||
return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
|
||||
};
|
||||
|
||||
if ($translate.statefulFilter()) {
|
||||
translateFilter.$stateful = true;
|
||||
}
|
||||
|
||||
return translateFilter;
|
||||
}
|
||||
translateFilterFactory.displayName = 'translateFilterFactory';
|
||||
app.filter('tr', translateFilterFactory);
|
||||
|
||||
app.controller('SetupAccountController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
const search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
const API_ORIGIN = window.cloudronApiOrigin || window.location.origin;
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.busy = false;
|
||||
$scope.error = null;
|
||||
$scope.view = 'setup';
|
||||
$scope.branding = null;
|
||||
$scope.dashboardUrl = '';
|
||||
|
||||
$scope.profileLocked = !!search.profileLocked;
|
||||
$scope.existingUsername = !!search.username;
|
||||
$scope.username = search.username || '';
|
||||
$scope.displayName = search.displayName || '';
|
||||
$scope.password = '';
|
||||
$scope.passwordRepeat = '';
|
||||
|
||||
$scope.onSubmit = function () {
|
||||
$scope.busy = true;
|
||||
$scope.error = null;
|
||||
|
||||
var data = {
|
||||
inviteToken: search.inviteToken,
|
||||
password: $scope.password
|
||||
};
|
||||
|
||||
if (!$scope.profileLocked) {
|
||||
if (!$scope.existingUsername) data.username = $scope.username;
|
||||
data.displayName = $scope.displayName;
|
||||
}
|
||||
|
||||
function error(data, status) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (status === 401) {
|
||||
$scope.view = 'invalidToken';
|
||||
} else if (status === 409) {
|
||||
$scope.error = {
|
||||
username: true,
|
||||
message: 'Username already taken'
|
||||
};
|
||||
$scope.setupAccountForm.username.$setPristine();
|
||||
setTimeout(function () { $('#inputUsername').focus(); }, 200);
|
||||
} else if (status === 400) {
|
||||
$scope.error = {
|
||||
message: data.message
|
||||
};
|
||||
if (data.message.indexOf('Username') === 0) {
|
||||
$scope.setupAccountForm.username.$setPristine();
|
||||
$scope.error.username = true;
|
||||
}
|
||||
} else {
|
||||
$scope.error = { message: 'Unknown error. Please try again later.' };
|
||||
console.error(status, data);
|
||||
}
|
||||
}
|
||||
|
||||
$http.post(API_ORIGIN + '/api/v1/auth/setup_account', data).success(function (data, status) {
|
||||
if (status !== 201) return error(data, status);
|
||||
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = data.accessToken;
|
||||
|
||||
$scope.dashboardUrl = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
|
||||
$scope.view = 'done';
|
||||
}).error(error);
|
||||
};
|
||||
|
||||
if (!$scope.existingUsername && $scope.profileLocked) {
|
||||
$scope.view = 'noUsername';
|
||||
$scope.initialized = true;
|
||||
} else {
|
||||
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
|
||||
$scope.initialized = true;
|
||||
|
||||
if (status !== 200) return;
|
||||
|
||||
if (data.language) $translate.use(data.language);
|
||||
|
||||
$scope.branding = data;
|
||||
}).error(function () {
|
||||
$scope.initialized = false;
|
||||
});
|
||||
}
|
||||
}]);
|
||||
@@ -1,42 +0,0 @@
|
||||
/* This file contains helpers which should not be part of client.js */
|
||||
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').directive('passwordReveal', function () {
|
||||
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
|
||||
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
|
||||
|
||||
return {
|
||||
link: function (scope, elements) {
|
||||
var element = elements[0];
|
||||
|
||||
if (!element.parentNode) {
|
||||
console.error('Wrong password-reveal directive usage. Element has no parent.');
|
||||
return;
|
||||
}
|
||||
|
||||
var eye = document.createElement('i');
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
eye.style.width = '18px';
|
||||
eye.style.height = '18px';
|
||||
eye.style.position = 'relative';
|
||||
eye.style.float = 'right';
|
||||
eye.style.marginTop = '-24px';
|
||||
eye.style.marginRight = '10px';
|
||||
eye.style.cursor = 'pointer';
|
||||
|
||||
eye.addEventListener('click', function () {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
eye.innerHTML = svgEye;
|
||||
} else {
|
||||
element.type = 'password';
|
||||
eye.innerHTML = svgEyeSlash;
|
||||
}
|
||||
});
|
||||
|
||||
element.parentNode.style.position = 'relative';
|
||||
element.parentNode.insertBefore(eye, element.nextSibling);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
@charset "UTF-8";
|
||||
/* Slider */
|
||||
.slick-slider { position: relative; display: block; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -ms-touch-action: pan-y; touch-action: pan-y; -webkit-tap-highlight-color: transparent; }
|
||||
|
||||
.slick-list { position: relative; overflow: hidden; display: block; margin: 0; padding: 0; }
|
||||
.slick-list:focus { outline: none; }
|
||||
.slick-loading .slick-list { background: #fff url("./ajax-loader.gif") center center no-repeat; }
|
||||
.slick-list.dragging { cursor: pointer; cursor: hand; }
|
||||
|
||||
.slick-slider .slick-track { -webkit-transform: translate3d(0, 0, 0); -moz-transform: translate3d(0, 0, 0); -ms-transform: translate3d(0, 0, 0); -o-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); }
|
||||
|
||||
.slick-track { position: relative; left: 0; top: 0; display: block; }
|
||||
.slick-track:before, .slick-track:after { content: ""; display: table; }
|
||||
.slick-track:after { clear: both; }
|
||||
.slick-loading .slick-track { visibility: hidden; }
|
||||
|
||||
.slick-slide { float: left; height: 100%; min-height: 1px; display: none; }
|
||||
[dir="rtl"] .slick-slide { float: right; }
|
||||
.slick-slide img { display: block; }
|
||||
.slick-slide.slick-loading img { display: none; }
|
||||
.slick-slide.dragging img { pointer-events: none; }
|
||||
.slick-initialized .slick-slide { display: block; }
|
||||
.slick-loading .slick-slide { visibility: hidden; }
|
||||
.slick-vertical .slick-slide { display: block; height: auto; border: 1px solid transparent; }
|
||||
|
||||
/* Icons */
|
||||
@font-face { font-family: "slick"; src: url("./fonts/slick.eot"); src: url("./fonts/slick.eot?#iefix") format("embedded-opentype"), url("./fonts/slick.woff") format("woff"), url("./fonts/slick.ttf") format("truetype"), url("./fonts/slick.svg#slick") format("svg"); font-weight: normal; font-style: normal; }
|
||||
/* Arrows */
|
||||
.slick-prev, .slick-next { position: absolute; display: block; height: 20px; width: 20px; line-height: 0; font-size: 0; cursor: pointer; background: transparent; color: transparent; top: 50%; margin-top: -10px; padding: 0; border: none; outline: none; }
|
||||
.slick-prev:hover, .slick-prev:focus, .slick-next:hover, .slick-next:focus { outline: none; background: transparent; color: transparent; }
|
||||
.slick-prev:hover:before, .slick-prev:focus:before, .slick-next:hover:before, .slick-next:focus:before { opacity: 1; }
|
||||
.slick-prev.slick-disabled:before, .slick-next.slick-disabled:before { opacity: 0.25; }
|
||||
|
||||
.slick-prev:before, .slick-next:before { font-family: "slick"; font-size: 20px; line-height: 1; color: white; opacity: 0.75; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
||||
|
||||
.slick-prev { left: -25px; }
|
||||
[dir="rtl"] .slick-prev { left: auto; right: -25px; }
|
||||
.slick-prev:before { content: "←"; }
|
||||
[dir="rtl"] .slick-prev:before { content: "→"; }
|
||||
|
||||
.slick-next { right: -25px; }
|
||||
[dir="rtl"] .slick-next { left: -25px; right: auto; }
|
||||
.slick-next:before { content: "→"; }
|
||||
[dir="rtl"] .slick-next:before { content: "←"; }
|
||||
|
||||
/* Dots */
|
||||
.slick-slider { margin-bottom: 30px; }
|
||||
|
||||
.slick-dots { position: absolute; bottom: -45px; list-style: none; display: block; text-align: center; padding: 0; width: 100%; }
|
||||
.slick-dots li { position: relative; display: inline-block; height: 20px; width: 20px; margin: 0 5px; padding: 0; cursor: pointer; }
|
||||
.slick-dots li button { border: 0; background: transparent; display: block; height: 20px; width: 20px; outline: none; line-height: 0; font-size: 0; color: transparent; padding: 5px; cursor: pointer; }
|
||||
.slick-dots li button:hover, .slick-dots li button:focus { outline: none; }
|
||||
.slick-dots li button:hover:before, .slick-dots li button:focus:before { opacity: 1; }
|
||||
.slick-dots li button:before { position: absolute; top: 0; left: 0; content: "•"; width: 20px; height: 20px; font-family: "slick"; font-size: 6px; line-height: 20px; text-align: center; color: black; opacity: 0.25; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
||||
.slick-dots li.slick-active button:before { color: black; opacity: 0.75; }
|
||||
|
||||
/*# sourceMappingURL=slick.css.map */
|
||||
@@ -262,9 +262,7 @@
|
||||
"label": "Bind adgangskode",
|
||||
"url": "Server URL"
|
||||
},
|
||||
"title": "Directory Server",
|
||||
"description": "Cloudron kan fungere som en central brugerkatalogserver for eksterne programmer.",
|
||||
"enabled": "Aktiveret",
|
||||
"ipRestriction": {
|
||||
"description": "Begræns adgang til Directory Server til specifikke IP'er eller områder. Linjer, der starter med <code>#</code>, behandles som kommentarer.",
|
||||
"placeholder": "Linjeadskilt IP-adresse eller undernet",
|
||||
@@ -282,8 +280,6 @@
|
||||
"failed": "Følgende brugere blev ikke importeret:",
|
||||
"sendInviteCheckbox": "Send en e-mail med invitation til importerede brugere"
|
||||
},
|
||||
"title": "Brugerkatalog",
|
||||
"newUserAction": "Ny bruger",
|
||||
"users": {
|
||||
"user": "Bruger",
|
||||
"groups": "Grupper",
|
||||
@@ -300,12 +296,10 @@
|
||||
"transferOwnershipTooltip": "Overdragelse af ejerskab",
|
||||
"invitationTooltip": "Inviter bruger",
|
||||
"mailmanagerTooltip": "Denne bruger kan administrere brugere og postkasser",
|
||||
"count": "Antal brugere i alt: {{ count }}",
|
||||
"setGhostTooltip": "Udgive sig for at være"
|
||||
},
|
||||
"groups": {
|
||||
"title": "Grupper",
|
||||
"newGroupAction": "Ny gruppe",
|
||||
"name": "Navn",
|
||||
"users": "Brugere",
|
||||
"externalLdapTooltip": "Fra ekstern LDAP-mappe"
|
||||
@@ -474,12 +468,10 @@
|
||||
"name": "Navn",
|
||||
"noPasswordsPlaceholder": "Der er ikke oprettet nogen app-passwords",
|
||||
"description": "App-adgangskoder er en sikkerhedsforanstaltning til beskyttelse af din Cloudron-brugerkonto. Hvis du har brug for at få adgang til en Cloudron-app fra en mobilapp eller klient, som du ikke har tillid til, kan du logge ind med dit brugernavn og den alternative adgangskode, der er genereret her.",
|
||||
"newPassword": "Nyt kodeord",
|
||||
"deletePasswordTooltip": "Slet adgangskode"
|
||||
},
|
||||
"apiTokens": {
|
||||
"title": "API-tokens",
|
||||
"newApiToken": "Nyt API-token",
|
||||
"name": "Navn",
|
||||
"expiresAt": "Udløber på",
|
||||
"description": "Brug disse personlige adgangstokens til at autentificere dig på <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>",
|
||||
@@ -581,7 +573,6 @@
|
||||
"cifsSealSupport": "Brug kryptering af segl. Kræver mindst SMB v3",
|
||||
"title": "Konfigurere backuplagring",
|
||||
"provider": "Udbyder af lagerplads",
|
||||
"noopNote": "Denne indstilling ødelægger Cloudrons backup- og gendannelsesfunktionalitet og bør kun bruges til testformål. Sørg for, at serveren er sikkerhedskopieret fuldstændigt ved hjælp af alternative midler.",
|
||||
"mountPoint": "Monteringspunkt",
|
||||
"mountPointDescription": "Mountpunktet skal oprettes manuelt. Se <a href=\"{{ providerDocsLink }}\" target=\"_blank\">docs</a>.",
|
||||
"localDirectory": "Lokal backup-mappe",
|
||||
@@ -703,8 +694,6 @@
|
||||
"bounceInfo": "Afsendelse af afvisning",
|
||||
"inboundInfo": "Modtaget",
|
||||
"outboundInfo": "I kø til levering",
|
||||
"receivedInfo": "Gemte",
|
||||
"deliveredInfo": "Afleveret post",
|
||||
"deniedInfo": "Forbindelse nægtet",
|
||||
"spamFilterTrainedInfo": "Spam-filter trænet ved hjælp af postkassens indhold",
|
||||
"underQuotaInfo": "Postkasse {{ mailbox }}} er faldet under {{ quotaPercent }}% quota",
|
||||
@@ -868,12 +857,8 @@
|
||||
"stopUpdateAction": "Stop opdatering"
|
||||
},
|
||||
"privateDockerRegistry": {
|
||||
"description": "Cloudron kan trække og installere <a href=\"{{ customAppsLink }}\" target=\"_blank\">brugerdefinerede apps</a>fra et privat docker-register.",
|
||||
"title": "Privat Docker-register",
|
||||
"subscriptionRequired": "Denne funktion er kun tilgængelig i de betalte abonnementer.",
|
||||
"setupSubscriptionAction": "Oprettelse af abonnement nu",
|
||||
"server": "Serveradresse",
|
||||
"username": "Brugernavn",
|
||||
"usernameNotSet": "Ikke indstillet",
|
||||
"configureAction": "Konfigurere register",
|
||||
"serverNotSet": "Ikke indstillet"
|
||||
@@ -905,20 +890,13 @@
|
||||
"title": "Cloudron.io-konto",
|
||||
"description": "En Cloudron.io-konto bruges til at få adgang til App Store og administrere dit abonnement.",
|
||||
"setupAction": "Oprettelse af konto",
|
||||
"email": "Konto e-mail",
|
||||
"subscription": "Abonnement",
|
||||
"cloudronId": "Cloudron ID",
|
||||
"subscriptionEndsAt": "Annulleret og slutter den",
|
||||
"subscriptionSetupAction": "Opgrader til Premium",
|
||||
"subscriptionChangeAction": "Ændre abonnement",
|
||||
"subscriptionReactivateAction": "Genaktivere abonnementet",
|
||||
"emailNotVerified": "E-mail endnu ikke bekræftet"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"title": "Konfiguration af det private register",
|
||||
"email": "E-mail (valgfrit)",
|
||||
"passwordToken": "Adgangskode/Token"
|
||||
},
|
||||
"registryConfig": {
|
||||
"provider": "Docker Registry Provider",
|
||||
"providerOther": "Andre",
|
||||
@@ -930,7 +908,6 @@
|
||||
"emailNotVerified": "Din cloudron.io-konto e-mail {{ email }} er ikke verificeret. Bekræft den venligst for at åbne supportbilletter.",
|
||||
"title": "Ticket",
|
||||
"subscriptionRequired": "Supportbilletter er kun tilgængelige i de betalte abonnementer.",
|
||||
"subscriptionRequiredDescription": "Du kan finde svar i vores <a href=\"{{{ supportViewLink }}\" target=\"_blank\">dokumentation</a> eller spørge på <a href=\"{{ forumLink }}\" target=\"_blank\">Forum</a>.",
|
||||
"type": "Type",
|
||||
"typeApp": "App-fejl",
|
||||
"typeBug": "Fejlrapport",
|
||||
@@ -978,14 +955,12 @@
|
||||
"title": "CPU-forbrug",
|
||||
"graphTitle": "Procentdel"
|
||||
},
|
||||
"title": "Systemoplysninger",
|
||||
"systemMemory": {
|
||||
"title": "System Memory",
|
||||
"graphSubtext": "Kun apps, der bruger mere end {{ threshold }} af memory, vises"
|
||||
},
|
||||
"selectPeriodLabel": "Vælg periode",
|
||||
"info": {
|
||||
"platformVersion": "Platformsversion",
|
||||
"title": "Info",
|
||||
"vendor": "Leverandør",
|
||||
"product": "Produtk",
|
||||
@@ -1065,7 +1040,6 @@
|
||||
"ovhAppSecret": "Application Secret"
|
||||
},
|
||||
"title": "Domæner og certs",
|
||||
"addDomain": "Tilføj domæne",
|
||||
"domain": "Domæne",
|
||||
"provider": "Udbyder",
|
||||
"tooltipEdit": "Rediger domæne",
|
||||
@@ -1266,12 +1240,9 @@
|
||||
"catchall": {
|
||||
"title": "Catch-all",
|
||||
"description": "E-mails, der sendes til ikke-eksisterende adresser, vil blive videresendt til følgende postkasser.",
|
||||
"subscriptionRequired": "Denne funktion er kun tilgængelig i de betalte abonnementer.<a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Oprettelse af abonnement nu</a>",
|
||||
"saveAction": "Gem"
|
||||
},
|
||||
"incomingServerInfo": "Indgående post (IMAP)",
|
||||
"enabled": "Cloudron Email Server er konfigureret til at modtage indgående e-mails for dette domæne.",
|
||||
"disabled": "Cloudron Email Server vil ikke modtage indgående e-mails for dette domæne.",
|
||||
"howToConnectDescription": "Brug indstillingerne nedenfor til at konfigurere e-mail-klienter.",
|
||||
"incomingUserInfo": "Brugernavn",
|
||||
"incomingPasswordUsage": "Adgangskode for ejeren af postkassen"
|
||||
@@ -1295,7 +1266,6 @@
|
||||
"noopAdminDomainWarning": "Cloudron kan ikke sende brugerinvitationer, nulstilling af adgangskode og andre meddelelser, når e-mail er deaktiveret på det primære domæne"
|
||||
},
|
||||
"signature": {
|
||||
"subscriptionRequired": "Denne funktion er kun tilgængelig i de betalte abonnementer.<a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Oprettelse af abonnement nu</a>",
|
||||
"title": "Signatur",
|
||||
"description": "Teksten her vil blive vedhæftet alle e-mails, der sendes ud fra dette domæne.",
|
||||
"plainTextFormat": "Tekstformat",
|
||||
@@ -1303,11 +1273,7 @@
|
||||
"saveAction": "Gem"
|
||||
},
|
||||
"smtpStatus": {
|
||||
"outboudRelay": "Udgående SMTP (Relay)",
|
||||
"notBlacklisted": "Denne servers IP {{ ip }} er<b>ikke</b>på en blokliste.",
|
||||
"title": "SMTP-status",
|
||||
"outboudDirect": "Udgående SMTP (direkte)",
|
||||
"blacklistCheck": "Kontrol af IP-adresse på blokliste",
|
||||
"blacklisted": "Denne servers IP {{ ip }} er på en blokliste."
|
||||
},
|
||||
"subscriptionDialog": {
|
||||
@@ -1535,14 +1501,6 @@
|
||||
"updateAvailableAction": "Opdatering tilgængelig",
|
||||
"installedAt": "Installeret på"
|
||||
},
|
||||
"auto": {
|
||||
"description": "Cloudron tjekker med jævne mellemrum <a href=»{{ appStoreLink }}« target=»_blank«>App Store</a> for opdateringer.",
|
||||
"title": "Automatiske opdateringer",
|
||||
"enabled": "Automatiske opdateringer er i øjeblikket aktiveret.",
|
||||
"disabled": "Automatiske opdateringer er i øjeblikket deaktiveret.",
|
||||
"disableAction": "Deaktivere automatiske opdateringer",
|
||||
"enableAction": "Aktivere automatiske opdateringer"
|
||||
},
|
||||
"noUpdates": "Ingen nye opdateringer tilgængelige"
|
||||
},
|
||||
"backups": {
|
||||
@@ -1550,7 +1508,6 @@
|
||||
"description": "Sikkerhedskopier er komplette snapshots af appen. Du kan bruge app-backups til at gendanne eller klone denne app.",
|
||||
"downloadBackupTooltip": "Download Sikkerhedskopi",
|
||||
"title": "Sikkerhedskopiering",
|
||||
"packageVersion": "Pakkeversion",
|
||||
"time": "Oprettet på",
|
||||
"downloadConfigTooltip": "Download Backup-konfiguration",
|
||||
"cloneTooltip": "Klon fra denne sikkerhedskopi",
|
||||
@@ -1575,9 +1532,7 @@
|
||||
"recovery": {
|
||||
"description": "Hvis appen ikke reagerer, skal du prøve at genstarte appen. Hvis appen konstant genstarter på grund af et defekt plugin eller en forkert konfiguration, skal du sætte appen i genoprettelsestilstand for at få adgang til konsollen.\nBrug følgende <a href=\"{{ docsLink }}\" target=\"_blank\">instruktioner</a> for at få appen til at køre igen.",
|
||||
"title": "Genopretning efter nedbrud",
|
||||
"restartAction": "Genstart appen",
|
||||
"enableRecoveryModeAction": "Aktiver genoprettelsestilstand",
|
||||
"disableRecoveryModeAction": "Deaktivere genoprettelsestilstand"
|
||||
"restartAction": "Genstart appen"
|
||||
},
|
||||
"taskError": {
|
||||
"title": "Opgavefejl",
|
||||
@@ -1682,7 +1637,7 @@
|
||||
},
|
||||
"uninstallDialog": {
|
||||
"title": "Afinstaller {{ app }}",
|
||||
"description": "Dette vil straks afinstallere <b>{{{ app }}</b> og fjerne alle dens data.",
|
||||
"description": "Dette vil straks afinstallere {{ app }} og fjerne alle dens data.",
|
||||
"uninstallAction": "Afinstaller"
|
||||
},
|
||||
"domainCollisionDialog": {
|
||||
@@ -1853,7 +1808,6 @@
|
||||
"title": "Fjern virkelig {{ volume }} ?",
|
||||
"removeAction": "Fjern"
|
||||
},
|
||||
"addVolumeAction": "Tilfoj Volume",
|
||||
"hostPath": "Target",
|
||||
"name": "Navn",
|
||||
"openFileManagerActionTooltip": "Åbn FileManager",
|
||||
@@ -1882,7 +1836,7 @@
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
"description": "Apps kan få adgang til monterede <a href=\"/#/volumes\">volumes</a> via mappen <code>/media/{volumes navn}</code>. Disse data er ikke medtaget i appens sikkerhedskopi."
|
||||
"description": "Apps kan få adgang til monterede <a href=\"/#/volumes\">volumes</a> via mappen <code>/media/(volumes navn)</code>. Disse data er ikke medtaget i appens sikkerhedskopi."
|
||||
}
|
||||
},
|
||||
"eventlog": {
|
||||
@@ -1961,11 +1915,6 @@
|
||||
"keysEndpoint": "Nøgler Slutpunkt",
|
||||
"tokenEndpoint": "Token slutpunkt",
|
||||
"authEndpoint": "Auth-slutpunkt"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Klienter",
|
||||
"newClient": "Ny klient",
|
||||
"empty": "Ingen klienten endnu"
|
||||
}
|
||||
},
|
||||
"automation": "Automatisering"
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
"close": "Schließen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"delete": "Löschen"
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"done": "Erledigt"
|
||||
},
|
||||
"username": "Username",
|
||||
"displayName": "Name",
|
||||
@@ -148,17 +150,13 @@
|
||||
"trustedIpRanges": "Vertrauenswürdige IPs & IP-Bereichen "
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"title": "System",
|
||||
"language": {
|
||||
"title": "Sprache",
|
||||
"description": "Die Standardsprache dieser Cloudron-Instanz kann hier ausgewählt werden. Diese Sprache wird auch für System-E-Mails wie Usereinladung und Passwort zurücksetzen verwendet. User können ihre bevorzugte Sprache für das Dashboard individuell im Profil ändern."
|
||||
},
|
||||
"privateDockerRegistry": {
|
||||
"username": "Username",
|
||||
"configureAction": "Register konfigurieren",
|
||||
"server": "Server-Adresse",
|
||||
"title": "Privates Docker-Register",
|
||||
"description": "Cloudron kann <a href=\"{{ customAppsLink }}\" target=\"_blank\">benutzerdefinierte Anwendungen</a> aus einem privaten Docker-Register laden und installieren.",
|
||||
"subscriptionRequired": "Diese Funktion ist nur im Abo enthalten.",
|
||||
"setupSubscriptionAction": "Abonnenement jetzt abschließen",
|
||||
"usernameNotSet": "Nicht gesetzt",
|
||||
@@ -178,9 +176,7 @@
|
||||
},
|
||||
"appstoreAccount": {
|
||||
"title": "Cloudron.io-Konto",
|
||||
"email": "E-Mail-Adresse",
|
||||
"description": "Ein Cloudron.io-Konto wird für den Zugriff auf den App-Store und die Verwaltung des Abonnements verwendet.",
|
||||
"subscriptionSetupAction": "Abonnement einrichten",
|
||||
"cloudronId": "Cloudron-ID",
|
||||
"subscriptionChangeAction": "Abonnement verwalten",
|
||||
"setupAction": "Konto einrichten",
|
||||
@@ -189,11 +185,6 @@
|
||||
"subscriptionEndsAt": "Gekündigt - endet am",
|
||||
"emailNotVerified": "E-Mail noch nicht verifiziert"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"passwordToken": "Passwort/Token",
|
||||
"email": "E-Mail-Adresse (optional)",
|
||||
"title": "Privates Register konfigurieren"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"hours": "Stunden",
|
||||
"disableCheckbox": "Automatische Aktualisierung deaktivieren",
|
||||
@@ -223,7 +214,6 @@
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"title": "User Verzeichnis",
|
||||
"externalLdap": {
|
||||
"errorSelfSignedCert": "Server benutzt ein ungültiges selbst signiertes Zertifikat.",
|
||||
"bindPassword": "Bind Passwort (optional)",
|
||||
@@ -264,8 +254,8 @@
|
||||
"externalLdapTooltip": "Aus externem LDAP Verzeichnis",
|
||||
"users": "User",
|
||||
"name": "Name",
|
||||
"newGroupAction": "Neue Gruppe",
|
||||
"title": "Gruppen"
|
||||
"title": "Gruppen",
|
||||
"emptyPlaceholder": "Noch keine Gruppen"
|
||||
},
|
||||
"users": {
|
||||
"removeUserTooltip": "User löschen",
|
||||
@@ -283,10 +273,8 @@
|
||||
"transferOwnershipTooltip": "Besitzer*in wechseln",
|
||||
"invitationTooltip": "User einladen",
|
||||
"mailmanagerTooltip": "Dieser User kann Benutzer und Postfächer verwalten.",
|
||||
"setGhostTooltip": "Als anderer User ausgeben",
|
||||
"count": "User insgesamt: {{ count }}"
|
||||
"setGhostTooltip": "Als anderer User ausgeben"
|
||||
},
|
||||
"newUserAction": "Neuer User",
|
||||
"role": {
|
||||
"owner": "Superadmin",
|
||||
"admin": "Administrationsrolle",
|
||||
@@ -408,8 +396,6 @@
|
||||
"label": "Zugriff beschränken",
|
||||
"placeholder": "Zeilen separierte IP Adresse oder Subnetz"
|
||||
},
|
||||
"enabled": "Aktiviert",
|
||||
"title": "Verzeichnis Server",
|
||||
"cloudflarePortWarning": "Cloudflare Proxying für die Dashboarddomäne muss deaktiviert sein um den LDAP Server zu erreichen"
|
||||
},
|
||||
"invitationNotification": {
|
||||
@@ -477,7 +463,6 @@
|
||||
"name": "Name",
|
||||
"noPasswordsPlaceholder": "Es sind bislang keine App-Passwörter erstellt worden.",
|
||||
"deletePasswordTooltip": "Passwort löschen",
|
||||
"newPassword": "Neues Passwort",
|
||||
"description": "App-Passwörter sind eine Sicherheitsmaßnahme zum Schutz des Cloudron-User-Kontos. Sobald eingerichtet, kann die Anmeldung (zusätzlich) mit dem Usernamen und dem hier angezeigtem Passwort erfolgen. Hinweis: sinnvoll bei nicht vertrauenswürdigen mobilen Anwendungen oder Desktop-Clients.",
|
||||
"title": "App-Passwörter"
|
||||
},
|
||||
@@ -491,7 +476,8 @@
|
||||
"errorNameRequired": "Ein Name ist erforderlich",
|
||||
"name": "Name des API-Token",
|
||||
"title": "API-Token erstellen",
|
||||
"access": "API Zugriff"
|
||||
"access": "API Zugriff",
|
||||
"allowedIpRanges": "Erlaubte IP-Bereich(e)"
|
||||
},
|
||||
"createAppPassword": {
|
||||
"generatePassword": "Passwort generieren",
|
||||
@@ -535,13 +521,14 @@
|
||||
"description": "Persönlichen Zugriffstoken zur Authentifizierung gegenüber der <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a> verwenden",
|
||||
"expiresAt": "Verfällt am",
|
||||
"name": "Name",
|
||||
"newApiToken": "Neuer API-Token",
|
||||
"title": "API-Tokens",
|
||||
"lastUsed": "Zuletzt Verwendet",
|
||||
"neverUsed": "nie",
|
||||
"scope": "Bereich",
|
||||
"readonly": "Schreibgeschützt",
|
||||
"readwrite": "Schreiben und Lesen"
|
||||
"readwrite": "Schreiben und Lesen",
|
||||
"allowedIpRangesPlaceholder": "Komma getrennte IPs oder Subnetze",
|
||||
"allowedIpRanges": "Erlaubte IPs"
|
||||
},
|
||||
"passwordRecoveryEmail": "Alternative E-Mail-Adresse",
|
||||
"passwordResetAction": "Passwort vergessen",
|
||||
@@ -552,7 +539,13 @@
|
||||
"changeBackgroundImage": {
|
||||
"title": "Hintergrundbild setzen"
|
||||
},
|
||||
"enable2FANotAvailable": "Für externe User nicht verfügbar"
|
||||
"enable2FANotAvailable": "Für externe User nicht verfügbar",
|
||||
"removeApiToken": {
|
||||
"title": "Token {{ name }} wirklich entfernen?"
|
||||
},
|
||||
"removeAppPassword": {
|
||||
"title": "Dieses Password wirklich entfernen?"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-Mail",
|
||||
@@ -563,7 +556,7 @@
|
||||
"info": "Die Einstellungen sind global und werden bei allen Domains verwendet.",
|
||||
"title": "Einstellungen",
|
||||
"spamFilterOverview": "{{ blacklistCount }} Adressen sind auf der Blockliste.",
|
||||
"solrFts": "Volltextsuche (Solr)",
|
||||
"solrFts": "Volltextsuche",
|
||||
"solrDisabled": "Deaktiviert",
|
||||
"changeDomainProgress": "E-Mail-Domäne ändern:",
|
||||
"solrEnabled": "Aktiviert",
|
||||
@@ -583,9 +576,9 @@
|
||||
"title": "Domains"
|
||||
},
|
||||
"solrConfig": {
|
||||
"title": "Volltextsuche (Solr)",
|
||||
"description": "Solr kann für schnelle Volltextsuche in Dovecot verwendet werden. Solr wird nur gestartet wenn der <a href=\"/#/services\" target=\"_blank\">E-Mail Dienst</a> mehr als 3GB Arbeitsspeicher zugewiesen hat.",
|
||||
"enableSolrCheckbox": "Volltextsuche mit Solr aktivieren",
|
||||
"title": "Volltextsuche",
|
||||
"description": "Solr & Tika kann für schnelle Volltextsuche in Dovecot verwendet werden. Solr wird nur gestartet wenn der <a href=\"/#/services\" target=\"_blank\">E-Mail Dienst</a> mehr als 3GB Arbeitsspeicher zugewiesen hat.",
|
||||
"enableSolrCheckbox": "Volltextsuche aktivieren",
|
||||
"notEnoughMemory": "Mindestens 3GB Arbeitsspeicher dem E-Mail Dienst zuweisen um Solr aktivieren zu können."
|
||||
},
|
||||
"eventlog": {
|
||||
@@ -593,22 +586,22 @@
|
||||
"type": {
|
||||
"bounceInfo": "Bounce-Mail gesendet",
|
||||
"deferred": "Zurückgestellt",
|
||||
"outboundInfo": "Zur Zustellung in die Warteschlange gestellt",
|
||||
"outboundInfo": "In der Warteschlange für den Versand",
|
||||
"denied": "Verweigert",
|
||||
"bounce": "Bounce",
|
||||
"incoming": "Eingehend",
|
||||
"queued": "Warteschlange",
|
||||
"deferredInfo": "Die Zustellung von E-Mail ist fehlgeschlagen. Wird in {{ details.delay }} Sekunden erneut versucht.",
|
||||
"deliveredInfo": "Zugestellte E-Mail",
|
||||
"receivedInfo": "Gespeichert",
|
||||
"deferredInfo": "Die Zustellung von E-Mail ist fehlgeschlagen. Wird in {{ delay }} Sekunden erneut versucht.",
|
||||
"deniedInfo": "Verbindung verweigert",
|
||||
"spamFilterTrainedInfo": "Der Spam-Filter wird durch Mailbox-Inhalte trainiert",
|
||||
"inboundInfo": "Eingehend",
|
||||
"inboundInfo": "In der Warteschlange für die eingehende Lieferung",
|
||||
"outgoing": "Ausgehend",
|
||||
"spamFilterTrained": "Spam-Filter trainiert",
|
||||
"underQuotaInfo": "Postfach {{ mailbox }} hat {{ quotaPercent }}% Speicherplatzbelegung unterschritten",
|
||||
"quota": "Postfach-Speicherplatz",
|
||||
"overQuotaInfo": "Postfach {{ mailbox }} ist zu {{ quotaPercent }}% voll"
|
||||
"overQuotaInfo": "Postfach {{ mailbox }} ist zu {{ quotaPercent }}% voll",
|
||||
"savedInfo": "Gespeichert",
|
||||
"sentInfo": "Gesendet"
|
||||
},
|
||||
"time": "Zeit",
|
||||
"searchPlaceholder": "Suche",
|
||||
@@ -623,7 +616,7 @@
|
||||
"description": "Dies zieht den E-Mail Server auf die neue Domäne um.",
|
||||
"location": "Adresse",
|
||||
"title": "E-Mail-Server Standort ändern",
|
||||
"manualInfo": "Manuell einen A-Eintrag für {{ Domain }} zur öffentlichen IP dieses Cloudrons hinzufügen"
|
||||
"manualInfo": "Manuell A (IPv4) und AAAA (IPv6) DNS-Einträge für <b>{{ domain }}</b> einrichten, die auf diesen Server verweisen."
|
||||
},
|
||||
"changeMailSizeDialog": {
|
||||
"description": "Das Ändern der maximalen E-Mail-Nachrichtengröße erfordert einen Neustart des Mailservers.",
|
||||
@@ -695,7 +688,6 @@
|
||||
"typeApp": "Anwendungsfehler",
|
||||
"typeBug": "Fehlermeldung",
|
||||
"report": "Meldung",
|
||||
"subscriptionRequiredDescription": "Antworten auf die häufigsten Fragen sind in der <a href=\"{{ supportViewLink }}\" target=\"_blank\">Dokumentation</a> verfügbar. Unser <a href=\"{{ forumLink }}\" target=\"_blank\">Forum</a> bietet einen Platz in die Community einzusteigen und sich auszutauschen.",
|
||||
"emailVerifyAction": "Jetzt verifizieren",
|
||||
"emailNotVerified": "Ihre cloudron.io Konto E-Mail {{ email }} ist nicht verifiziert. Bitte bestätigen Sie Ihre E-Mail Adresse, um Support-Tickets zu öffnen.",
|
||||
"typeBilling": "Problem mit Rechnung"
|
||||
@@ -736,7 +728,7 @@
|
||||
"namecheapInfo": "Die Server-IP-Adresse muss für diesen API-Schlüssel auf der Erlaubtliste stehen.",
|
||||
"fallbackCertCertificatePlaceholder": "Zertifikat",
|
||||
"nameComApiToken": "API-Token",
|
||||
"wildcardInfo": "<i>A</i>-Eintrag für <b>*.{{ domain }}</b> und <b>{{ domain }}</b> mit der IP-Adresse dieser Cloudron-Instanz einrichten.",
|
||||
"wildcardInfo": "Manuell A (IPv4) und AAAA (IPv6) DNS-Einträge für <b>{{ domain }}</b> einrichten, die auf diesen Server verweisen",
|
||||
"letsEncryptInfo": "Let's Encrypt erfordert, dass der Server auf Port 80 erreichbar ist",
|
||||
"advancedAction": "Erweiterte Einstellungen…",
|
||||
"zoneName": "Zonen-Namen (optional)",
|
||||
@@ -764,15 +756,15 @@
|
||||
"goDaddyApiKey": "API-Schlüssel",
|
||||
"cloudflareEmail": "Cloudflare-E-Mail",
|
||||
"linodeToken": "Linode-Token",
|
||||
"mastodonHostname": "Mastodon Server-Domain",
|
||||
"matrixHostname": "Matrix Server-Pfad",
|
||||
"mastodonHostname": "Mastodon Domain",
|
||||
"matrixHostname": "Matrix Domain",
|
||||
"netcupApiPassword": "API Passwort",
|
||||
"netcupApiKey": "API Key",
|
||||
"netcupCustomerNumber": "Kundennummer",
|
||||
"vultrToken": "Vultr Token",
|
||||
"wellKnownDescription": "Die Werte werden von Cloudron verwendet, um auf <code>/.well-known/</code> URLs zu antworten. Beachten Sie, dass eine App auf der bloßen Domain <code>{{ domain }}</code> verfügbar sein muss, damit dies funktioniert. Siehe die <a href=\"{{docsLink}}\" target=\"_blank\">Dokumentation</a> für weitere Informationen.",
|
||||
"hetznerToken": "Hetzner Token",
|
||||
"jitsiHostname": "Jitsi Pfad",
|
||||
"jitsiHostname": "Jitsi Domain",
|
||||
"cloudflareDefaultProxyStatus": "Proxying für neue DNS-Einträge aktivieren",
|
||||
"porkbunSecretapikey": "Geheimer API-Schlüssel",
|
||||
"porkbunApikey": "API-Schlüssel",
|
||||
@@ -785,7 +777,10 @@
|
||||
"ovhAppSecret": "Application Secret",
|
||||
"gandiTokenType": "Tokentyp",
|
||||
"gandiTokenTypeApiKey": "API Schlüssel (veraltet)",
|
||||
"gandiTokenTypePAT": "Persönliches Zugriffstoken (PAT)"
|
||||
"gandiTokenTypePAT": "Persönliches Zugriffstoken (PAT)",
|
||||
"customNameservers": "Domäne nutzt benutzerdefinierte (Vanity) Nameserver",
|
||||
"inwxPassword": "Password",
|
||||
"inwxUsername": "Username"
|
||||
},
|
||||
"changeDashboardDomain": {
|
||||
"title": "Die Dashboard-Domäne ändern",
|
||||
@@ -795,7 +790,6 @@
|
||||
"cancelAction": "Abbrechen"
|
||||
},
|
||||
"domain": "Domäne",
|
||||
"addDomain": "Domäne hinzufügen",
|
||||
"provider": "Anbieter",
|
||||
"tooltipEdit": "Domäne bearbeiten",
|
||||
"tooltipRemove": "Domäne entfernen",
|
||||
@@ -826,10 +820,24 @@
|
||||
"nonePending": "Alles erledigt!",
|
||||
"dismissTooltip": "Verwerfen",
|
||||
"clearAll": "Alles löschen",
|
||||
"markAllAsRead": "Alle als gelesen markieren"
|
||||
"markAllAsRead": "Alle als gelesen markieren",
|
||||
"settings": {
|
||||
"rebootRequired": "Serverneustart benötigt",
|
||||
"title": "Benachrichtigungseinstellungen",
|
||||
"backupFailed": "Datensicherung fehlgeschlagen",
|
||||
"certificateRenewalFailed": "Zertifikatserneuerung fehlgeschlagen",
|
||||
"appOutOfMemory": "App ist der Arbeitsspeicher ausgegangen",
|
||||
"appUp": "App ist wieder online",
|
||||
"appDown": "App funktioniert nicht",
|
||||
"cloudronUpdateFailed": "Cloudron Aktualisierung fehlgeschlagen",
|
||||
"diskSpace": "Wenig Speicherplatz"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "Eine E-Mail wird für die ausgewählten Ereignisse an Ihre primäre E-Mail-Adresse gesendet."
|
||||
},
|
||||
"allCaughtUp": "Alles erledigt"
|
||||
},
|
||||
"system": {
|
||||
"title": "Systeminformationen",
|
||||
"diskUsage": {
|
||||
"diskContent": "Dieser {{ type }} Datenträger enthält",
|
||||
"usageInfo": "{{ available | prettyDiskSize }}</b> von <b>{{ size | prettyDiskSize }}</b> verfügbar",
|
||||
@@ -847,12 +855,11 @@
|
||||
},
|
||||
"cpuUsage": {
|
||||
"graphTitle": "Anteil in Prozent",
|
||||
"title": "CPU-Auslastung",
|
||||
"title": "CPU",
|
||||
"graphSubtext": "Es werden nur Anwendungen angezeigt, die mehr als {{ threshold }} an Rechenleistung benötigen"
|
||||
},
|
||||
"selectPeriodLabel": "Zeitraum auswählen",
|
||||
"info": {
|
||||
"platformVersion": "Plattform Version",
|
||||
"title": "Info",
|
||||
"vendor": "Anbieter",
|
||||
"product": "Produkt",
|
||||
@@ -862,6 +869,9 @@
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Graphen"
|
||||
},
|
||||
"locale": {
|
||||
"title": "Locale-Einstellungen"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -871,7 +881,7 @@
|
||||
"memoryLimit": "Speicherlimit",
|
||||
"advancedSettings": "Erweiterte Einstellungen…",
|
||||
"encryptionDescription": "Vorsicht: Passphrase an einem sicheren Ort aufbewahren. Cloudron speichert dieses Passwort nicht. Backups können ohne die Passphrase nicht entschlüsselt werden",
|
||||
"encryptionPassword": "Verschlüsselungspasswort (optional)",
|
||||
"encryptionPassword": "Verschlüsselungspasswort",
|
||||
"s3LikeNote": "Bitte alle object expiration lifecycle Regeln entfernen, da dadurch rsync-Backups beschädigt werden.",
|
||||
"formatChangeNote": "Frühere Backups, die das alte Speicherformat verwenden, müssen manuell entfernt werden.",
|
||||
"format": "Speicherformat",
|
||||
@@ -888,7 +898,6 @@
|
||||
"localDirectory": "Lokales Backup-Verzeichnis",
|
||||
"mountPointDescription": "Der Mount-Point muss manuell gesetzt werden. Weitere Informationen in der <a href=\"{{ providerDocsLink }}\" target=\"_blank\">Anleitung</a>.",
|
||||
"mountPoint": "Mount-Point",
|
||||
"noopNote": "Diese Option unterbricht die Sicherungs- und Wiederherstellungsfunktionalität von Cloudron und sollte nur zu Testzwecken verwendet werden. Der Server muss in dieser Betriebsart vollständig mit alternativen Mitteln gesichert werden.",
|
||||
"provider": "Speicher-Anbieter",
|
||||
"title": "Backup-Speicher konfigurieren",
|
||||
"encryptionPasswordRepeat": "Password wiederholen",
|
||||
@@ -915,7 +924,8 @@
|
||||
"cifsSealSupport": "Verschlüsselung verwenden. Erfordert mindestens SMB v3",
|
||||
"encryptedFilenames": "Dateinamen verschlüsseln",
|
||||
"chown": "Entferntes Dateisystem unterstützt chown",
|
||||
"encryptFilenames": "Dateinamen verschlüsseln"
|
||||
"encryptFilenames": "Dateinamen verschlüsseln",
|
||||
"preserveAttributesLabel": "Dateiattribute erhalten"
|
||||
},
|
||||
"configureBackupSchedule": {
|
||||
"retentionPolicy": "Aufbewahrungsrichtlinie",
|
||||
@@ -956,7 +966,7 @@
|
||||
"version": "Version",
|
||||
"contents": "Inhalt",
|
||||
"noBackups": "Es wurde noch keine Datensicherung durchgeführt.",
|
||||
"title": "Liste vorhandener Datensicherungen",
|
||||
"title": "Datensicherungen",
|
||||
"tooltipEditBackup": "Datensicherung bearbeiten",
|
||||
"tooltipPreservedBackup": "Dieses Backup bleibt erhalten"
|
||||
},
|
||||
@@ -990,6 +1000,23 @@
|
||||
},
|
||||
"label": "Label",
|
||||
"remotePath": "Remote Pfad"
|
||||
},
|
||||
"archives": {
|
||||
"title": "Apparchiv",
|
||||
"info": "Info"
|
||||
},
|
||||
"deleteArchiveDialog": {
|
||||
"title": "Archiv von {{ appTitle }} ({{ fqdn }}) löschen",
|
||||
"description": "Nach dem Löschen wird die Datensicherung basierend der Aufbewahrungsrichtlinie bereinigt."
|
||||
},
|
||||
"restoreArchiveDialog": {
|
||||
"restoreActionOverwrite": "Wiederherstelle und DNS überschreiben",
|
||||
"title": "Von Archiv wiederherstellen",
|
||||
"description": "Dies installiert {{ appId }} auf der angegebenen Domäne mit der Datensicherung vom {{ creationTime }}.",
|
||||
"restoreAction": "Wiederherstellen"
|
||||
},
|
||||
"deleteArchive": {
|
||||
"deleteAction": "Löschen"
|
||||
}
|
||||
},
|
||||
"appstore": {
|
||||
@@ -1035,7 +1062,7 @@
|
||||
"userManagementMailbox": "Alle Nutzer mit einem Postfach auf diesem Cloudron haben Zugriff.",
|
||||
"userManagementNone": "Diese Anwendung verfügt über eine eigene User-Verwaltung. Diese Einstellung bestimmt die Sichtbarkeit der Anwendung im Dashboard.",
|
||||
"userManagement": "User-Verwaltung",
|
||||
"manualWarning": "Manuell einen DNS-A-Eintrag für <b>{{ location }}</b> erstellen, der auf die Cloudron-IP zeigt",
|
||||
"manualWarning": "Manuell A (IPv4) und AAAA (IPv6) DNS-Einträge für <b>{{ location }}</b> einrichten, die auf diesen Server verweisen.",
|
||||
"locationPlaceholder": "Leer lassen um Hauptdomäne zu benutzen",
|
||||
"location": "Domäne",
|
||||
"memoryRequirement": "Benötigt mindestens {{ size }} Arbeitsspeicher",
|
||||
@@ -1047,7 +1074,7 @@
|
||||
"unstable": "Unstable",
|
||||
"appMissing": "Fehlende Anwendungen bitte im Forum vorstellen.",
|
||||
"noAppsFound": "Keine passende Anwendung gefunden.",
|
||||
"searchPlaceholder": "Suche nach Alternativen wie Gihub, Dropbox, Slack, Trello, …",
|
||||
"searchPlaceholder": "Suche nach Alternativen wie Github, Dropbox, Slack, Trello, …",
|
||||
"category": {
|
||||
"newApps": "Neue Apps",
|
||||
"popular": "Beliebt",
|
||||
@@ -1100,7 +1127,7 @@
|
||||
"refresh": "Aktualisieren"
|
||||
},
|
||||
"branding": {
|
||||
"title": "Design- & Textanpassungen",
|
||||
"title": "Erscheinungsbild",
|
||||
"changeLogo": {
|
||||
"title": "Cloudron-Avatar auswählen"
|
||||
},
|
||||
@@ -1119,7 +1146,7 @@
|
||||
"password": "Passwort",
|
||||
"username": "Username",
|
||||
"errorIncorrectCredentials": "Falscher Username oder falsches Passwort",
|
||||
"2faToken": "2FA-Token (wenn aktiviert)",
|
||||
"2faToken": "2FA-Token",
|
||||
"loginTo": "Anmeldung bei",
|
||||
"signInAction": "Anmelden",
|
||||
"resetPasswordAction": "Passwort zurücksetzen",
|
||||
@@ -1146,8 +1173,7 @@
|
||||
},
|
||||
"email": {
|
||||
"signature": {
|
||||
"subscriptionRequired": "Diese Funktion ist nur im Abo enthalten. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Abonnenement jetzt abschließen</a>",
|
||||
"htmlFormat": "HTML-Format (optional)",
|
||||
"htmlFormat": "HTML-Format",
|
||||
"saveAction": "Speichern",
|
||||
"title": "Signatur",
|
||||
"description": "Der folgende Text wird an alle E-Mails angehängt, die von dieser Domäne ausgehen.",
|
||||
@@ -1173,7 +1199,6 @@
|
||||
},
|
||||
"incoming": {
|
||||
"catchall": {
|
||||
"subscriptionRequired": "Diese Funktion ist nur im Abo enthalten. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Abonnenement jetzt abschließen</a>",
|
||||
"title": "Catch-all",
|
||||
"description": "E-Mails, die an nicht vorhandene Adressen gesendet werden, werden an die folgenden Postfächer weitergeleitet.",
|
||||
"saveAction": "Speichern"
|
||||
@@ -1210,11 +1235,9 @@
|
||||
"outgointServerInfo": "Ausgehende E-Mails (SMTP)",
|
||||
"sieveServerInfo": "Sieve-Filter verwalten",
|
||||
"incomingServerInfo": "Eintreffende E-Mail (IMAP)",
|
||||
"enabled": "Der Cloudron Email-Server ist für den Empfang von eingehenden E-Mails für diese Domain konfiguriert.",
|
||||
"howToConnectDescription": "Verwenden Sie die folgenden Einstellungen, um E-Mail-Programme zu konfigurieren.",
|
||||
"incomingPasswordUsage": "Passwort des Besitzers der Mailbox",
|
||||
"incomingPasswordInfo": "Passwort",
|
||||
"disabled": "Der Cloudron Email Server nimmt keine eintreffenden Emails für diese Domain an.",
|
||||
"incomingUserInfo": "Benutzername"
|
||||
},
|
||||
"masquerading": {
|
||||
@@ -1225,11 +1248,9 @@
|
||||
},
|
||||
"smtpStatus": {
|
||||
"notBlacklisted": "Die IP-Adresse des Servers {{ ip }} ist <b>nicht</b> auf einer bekannten Blockliste.",
|
||||
"outboudRelay": "Ausgeheneder SMTP (Relay)",
|
||||
"blacklistCheck": "IP-Adressen Blocklisten-Überprüfung",
|
||||
"blacklisted": "Die IP-Adresse des Servers {{ ip }} ist auf einer Blockliste.",
|
||||
"title": "SMTP-Status",
|
||||
"outboudDirect": "Ausgehender SMTP (direkt)"
|
||||
"rblCheck": "DNS-Blocklist-Prüfung",
|
||||
"outboundSmtp": "Ausgehend SMTP"
|
||||
},
|
||||
"enableEmailDialog": {
|
||||
"description": "Dies wird Cloudron so konfigurieren, dass E-Mails für <b>{{ domain }}</b> empfangen werden. Die Dokumentation zum Öffnen der <a href=\"{{ requiredPortsDocsLink }}\" target=\"_blank\">erforderlichen Ports</a> für Cloudron E-Mail lesen.",
|
||||
@@ -1586,10 +1607,8 @@
|
||||
"repair": {
|
||||
"recovery": {
|
||||
"title": "Wiederherstellung nach einem Absturz",
|
||||
"enableRecoveryModeAction": "Wiederherstellungsmodus aktivieren",
|
||||
"description": "Wenn die Anwendung nicht antwortet, bitte einen Neustart versuchen. Wenn die Anwendung aufgrund eines defekten Plugins oder einer Fehlkonfiguration ständig neu gestartet wird, die Anwendung in den Wiederherstellungsmodus bringen, um auf die Konsole zuzugreifen.\nFolgende <a href=\"{{ docsLink }}\" target=\"_blank\">Hinweise</a> befolgen, um die Anwendung wieder zum Laufen zu bringen.",
|
||||
"restartAction": "Anwendung neustarten",
|
||||
"disableRecoveryModeAction": "Wiederherstellungsmodus deaktivieren"
|
||||
"restartAction": "Neustarten"
|
||||
},
|
||||
"taskError": {
|
||||
"description": "Wenn ein Konfigurations-, Aktualisierungs-, Wiederherstellungs- oder Sicherungsauftrag zu einem Fehler geführt hat, Auftrag erneut versuchen.",
|
||||
@@ -1601,13 +1620,13 @@
|
||||
"repairTabTitle": "Reparatur",
|
||||
"uninstall": {
|
||||
"startStop": {
|
||||
"startAction": "Anwendung starten",
|
||||
"stopAction": "Anwendung stoppen",
|
||||
"startAction": "Starten",
|
||||
"stopAction": "Stoppen",
|
||||
"title": "Starten / Stoppen",
|
||||
"description": "Anwendungen können angehalten werden, um Server-Ressourcen zu schonen, anstatt sie zu deinstallieren. Zukünftige Anwendungs-Backups werden keine Änderungen von Anwendungen zwischen jetzt und dem letzten Anwendungs-Backup enthalten. Aus diesem Grund wird empfohlen, vor dem Stoppen der Anwendung ein Backup auszulösen."
|
||||
},
|
||||
"uninstall": {
|
||||
"description": "Dies wird die Anwendung sofort deinstallieren und alle zugehörigen Daten löschen. Die Anwendung steht anschließend nicht mehr zur Verfügung.",
|
||||
"description": "Dies wird die Anwendung deinstallieren und alle zugehörigen Daten löschen. Datensicherungen werden basierend der Aufbewahrungseinstellungen bereinigt.",
|
||||
"title": "Deinstallieren",
|
||||
"uninstallAction": "Deinstallieren"
|
||||
}
|
||||
@@ -1622,14 +1641,6 @@
|
||||
"icon": "Symbol"
|
||||
},
|
||||
"updates": {
|
||||
"auto": {
|
||||
"enableAction": "Auto-Updates Aktivieren",
|
||||
"disabled": "Die automatische Aktualisierung ist deaktiviert.",
|
||||
"enabled": "Die automatische Aktualisierung ist aktiviert.",
|
||||
"title": "Automatische Aktualisierungen",
|
||||
"description": "Cloudron fragt regelmäßig den App-Store nach Aktualisierungen ab. Wenn automatisches Aktualisieren deaktiviert ist, bitte sicherstellen, dass manuell nach Aktualisierungen gesucht wird.",
|
||||
"disableAction": "Auto-Updates Deaktivieren"
|
||||
},
|
||||
"info": {
|
||||
"updateAvailableAction": "Aktualisierung verfügbar",
|
||||
"title": "Information über die Anwendung",
|
||||
@@ -1647,7 +1658,6 @@
|
||||
"backups": {
|
||||
"backups": {
|
||||
"title": "Backups",
|
||||
"packageVersion": "Paket-Version",
|
||||
"time": "Erstellt am",
|
||||
"downloadConfigTooltip": "Konfiguration herunterladen",
|
||||
"description": "Backups erstellen komplette Abbilder der Anwendung. Ein Anwendungsbackup kann zum Wiederherstellen oder Klonen dieser Anwendung verwendet werden.",
|
||||
@@ -1680,7 +1690,9 @@
|
||||
"sso": "Diese Anwendung ist für die Authentifizierung mit dem Cloudron-Userverzeichnis eingerichtet. Cloudron-User können sich einloggen und die Anwendung sofort benutzen.",
|
||||
"appDocsUrl": "Bitte die <a target=\"_blank\" href=\"{{ docsUrl }}\">{{ Titel }} Dokumentation</a> für hilfreiche Informationen und allgemeine Themen zu dieser Anwendung beachten. Weitere Hilfestellung ist im <a target=\"_blank\" href=\"{{ forumUrl }}\">{{ title }} Forum</a> von Cloudron zu finden.",
|
||||
"postInstallConfirmCheckbox": "Hinweise zur Kenntnis genommen",
|
||||
"checklist": "Admin Kontrollliste"
|
||||
"checklist": "Admin Kontrollliste",
|
||||
"checklistShow": "Checkliste anzeigen",
|
||||
"checklistHide": "Checkliste ausblenden"
|
||||
},
|
||||
"storage": {
|
||||
"appdata": {
|
||||
@@ -1692,7 +1704,7 @@
|
||||
"mountTypeWarning": "Das Zieldateisystem muss Dateiberechtigungen und Eigentümerschaft unterstützen, damit die Verschiebung funktioniert"
|
||||
},
|
||||
"mounts": {
|
||||
"title": "Mounts",
|
||||
"title": "Datenträger Mounts",
|
||||
"readOnly": "Read-Only",
|
||||
"volume": "Datenträger",
|
||||
"noMounts": "Es sind keine Datenträger gemounted.",
|
||||
@@ -1707,7 +1719,7 @@
|
||||
},
|
||||
"uninstallDialog": {
|
||||
"title": "{{ app }} deinstallieren",
|
||||
"description": "Dies wird <b>{{ app }}</b> sofort deinstallieren und alle Daten löschen.",
|
||||
"description": "Dies wird {{ app }} sofort deinstallieren und alle Daten löschen.",
|
||||
"uninstallAction": "Deinstallieren"
|
||||
},
|
||||
"domainCollisionDialog": {
|
||||
@@ -1729,7 +1741,9 @@
|
||||
"warning": "Alle Daten, die zwischen jetzt und der letzten bekannten Sicherung erzeugt wurden, gehen unwiderruflich verloren. Es wird empfohlen, ein Backup der aktuellen Daten zu erstellen, bevor eine Wiederherstellung versucht wird.",
|
||||
"restoreAction": "Wiederherstellen",
|
||||
"title": "{{ app }} wiederherstellen",
|
||||
"description": "Hierdurch wird diese Anwendung mit den Daten vom {{ creationTime }} wiederhergestellt."
|
||||
"description": "Hierdurch wird diese Anwendung mit den Daten vom {{ creationTime }} wiederhergestellt.",
|
||||
"cloneAction": "Klonen",
|
||||
"cloneActionOverwrite": "DNS klonen und DNS überschreiben"
|
||||
},
|
||||
"storageTabTitle": "Speicher",
|
||||
"location": {
|
||||
@@ -1743,7 +1757,8 @@
|
||||
"aliasesPlaceholder": "Leer lassen um Hauptdomäne zu benutzen",
|
||||
"noAliases": "Kein Alias konfiguriert.",
|
||||
"addAliasAction": "Alias hinzufügen",
|
||||
"aliases": "Aliasse"
|
||||
"aliases": "Aliasse",
|
||||
"dnsoverwrite": "Einige DNS-Einträge existieren bereits. Mit dem Überschreiben einverstanden."
|
||||
},
|
||||
"updateDialog": {
|
||||
"subscriptionExpired": "Das Cloudron-Abonnement ist abgelaufen. Bitte ein Abonnement einrichten, um die Anwendung zu aktualisieren.",
|
||||
@@ -1766,7 +1781,9 @@
|
||||
"7d": "7 Tage",
|
||||
"30d": "30 Tage",
|
||||
"12h": "12 Stunden",
|
||||
"6h": "6 Stunden"
|
||||
"6h": "6 Stunden",
|
||||
"live": "Live",
|
||||
"1h": "1 Stunde"
|
||||
},
|
||||
"memoryTitle": "Speicher (RAM + Swap) in MB",
|
||||
"networkIOTotal": "insgesamt: Eingehend {{ inbound }} / Ausgehend {{ outbound }}",
|
||||
@@ -1843,7 +1860,8 @@
|
||||
"redis": {
|
||||
"disable": "Redis deaktivieren",
|
||||
"title": "Redis Konfiguration",
|
||||
"enable": "Die App mit Redis vorkonfigurieren"
|
||||
"enable": "Die App mit Redis vorkonfigurieren",
|
||||
"info": "Wenn aktiviert, verwendet die App den integrierten Redis-Dienst. Wenn deaktiviert, bleiben die Redis-Einstellungen der App unberührt."
|
||||
},
|
||||
"infoTabTitle": "Info",
|
||||
"info": {
|
||||
@@ -1854,9 +1872,23 @@
|
||||
"turn": {
|
||||
"enable": "App für den internen TURN Server konfigurieren",
|
||||
"disable": "TURN Server dieser App nicht automatisch konfigurieren.",
|
||||
"title": "TURN Einstellungen"
|
||||
"title": "TURN Einstellungen",
|
||||
"info": "Aktivieren Sie diese Option, um die App so zu konfigurieren, dass der integrierte TURN-Server verwendet wird. Wenn deaktiviert, bleiben die TURN-Einstellungen der App unverändert."
|
||||
},
|
||||
"servicesTabTitle": "Dienste"
|
||||
"servicesTabTitle": "Dienste",
|
||||
"archive": {
|
||||
"title": "Archiv",
|
||||
"action": "Archiv",
|
||||
"noBackup": "Diese App hat keine Datensicherung. Archivierung benötigt eine aktuelle Datensicherung.",
|
||||
"description": "Die letzte Datensicherung der App wird dem <a href=\"/#/backups\">Archiv</a> hinzugefügt. Die App wird deinstalliert, aber kann im Datensicherungsbereich wiederhergestellt werden. Die anderen Datensicherungen werden basierend der Aufbewahrungseinstellungen bereinigt.",
|
||||
"latestBackupInfo": "Die letzte Datensicherung wurde am {{ date }} erstellt."
|
||||
},
|
||||
"archiveDialog": {
|
||||
"description": "Dies deinstalliert die App und legt die letzte Datensicherung, erstellt am {{ date }} ins Archiv.",
|
||||
"title": "Archiviere {{ app }}"
|
||||
},
|
||||
"configureTooltip": "Konfigurieren",
|
||||
"updateAvailableTooltip": "Aktualisierung verfügbar"
|
||||
},
|
||||
"logs": {
|
||||
"download": "Vollständige Logfiles herunterladen",
|
||||
@@ -1905,7 +1937,6 @@
|
||||
"openFileManagerActionTooltip": "File-Manager öffnen",
|
||||
"name": "Name",
|
||||
"hostPath": "Ziel",
|
||||
"addVolumeAction": "Datenträger hinzufügen",
|
||||
"title": "Datenträger",
|
||||
"mountType": "Einhängepunkttyp",
|
||||
"updateVolumeDialog": {
|
||||
@@ -1931,7 +1962,7 @@
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
"description": "Apps können auf gemountete <a href=\"/#/volumes\">Volumes</a> über das <code>/media/{volume name}</code> Verzeichnis zugreifen. Diese Daten sind nicht im Backup der App enthalten."
|
||||
"description": "Apps können auf gemountete <a href=\"/#/volumes\">Volumes</a> über das <code>/media/(volume name)</code> Verzeichnis zugreifen. Diese Daten sind nicht im Backup der App enthalten."
|
||||
}
|
||||
},
|
||||
"supportConfig": {
|
||||
@@ -1939,26 +1970,26 @@
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Client hinzufügen",
|
||||
"description": "Neuen OpenID Connect Clienten hinzufügen.",
|
||||
"createAction": "Erstellen"
|
||||
"title": "OpenID Client hinzufügen",
|
||||
"description": "Neuen OpenID Client hinzufügen.",
|
||||
"createAction": "Hinzufügen"
|
||||
},
|
||||
"client": {
|
||||
"name": "Name",
|
||||
"id": "Client ID",
|
||||
"signingAlgorithm": "Signatur Algorithmus",
|
||||
"loginRedirectUri": "Login Callback Url (bei mehreren mit Komma getrennt)",
|
||||
"loginRedirectUri": "Login Callback URLs (mit Komma getrennt)",
|
||||
"logoutRedirectUri": "Logout Callback Url (optional)",
|
||||
"secret": "Client Geheimnis"
|
||||
},
|
||||
"title": "OpenID Connect Provider",
|
||||
"title": "OpenID-Anbieter",
|
||||
"description": "Cloudron kann als OpenID Connect Provider für interne und externe Apps fungieren.",
|
||||
"editClientDialog": {
|
||||
"title": "Client {{ client }} bearbeiten"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Wirklich Client {{ client }} löschen?",
|
||||
"description": "Damit werden alle externen OpenID Apps, die diese Clientendetails nutzen, getrennt."
|
||||
"description": "Wenn dies gelöscht wird, werden alle Tokens dieses OpenID Clients, ungültig gemacht. Damit werden alle externen OpenID Apps, die diese Clientendetails nutzen, getrennt."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "Discovery URL",
|
||||
@@ -1967,12 +1998,15 @@
|
||||
"keysEndpoint": "Schlüssel Endpunkt",
|
||||
"tokenEndpoint": "Token Endpunkt",
|
||||
"authEndpoint": "Auth Endpunkt"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clienten",
|
||||
"newClient": "Neuer Client",
|
||||
"empty": "Noch keine Clienten erstellt"
|
||||
}
|
||||
},
|
||||
"automation": "Automatisierung"
|
||||
"automation": "Automatisierung",
|
||||
"userdirectory": {
|
||||
"settings": {
|
||||
"title": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Erscheinungsbild"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"userManagementMailbox": "Todos los usuarios con un buzón en este Cloudron tienen acceso.",
|
||||
"userManagementNone": "Esta aplicación tiene su propia gestión de usuarios. Esta configuración determina si esta aplicación está visible en el panel del usuario.",
|
||||
"userManagement": "Gestión de usuarios",
|
||||
"manualWarning": "Añadir un A record manualmente para <b>{{ location }}</b> a la IP pública de Cloudron",
|
||||
"manualWarning": "Configurar manualmente los registros DNS A (IPv4) y AAAA (IPv6) para <b>{{ location }}</b> que apuntan a este servidor",
|
||||
"locationPlaceholder": "Dejar vacío para usar solo el dominio",
|
||||
"location": "Localización",
|
||||
"memoryRequirement": "Requiere al menos {{ size }} de memoria",
|
||||
@@ -121,7 +121,9 @@
|
||||
"close": "Cerrar",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Borrar"
|
||||
"delete": "Borrar",
|
||||
"edit": "Editar",
|
||||
"done": "Hecho"
|
||||
},
|
||||
"logout": "Salir",
|
||||
"offline": "Cloudron está desconectado. Reconectando…",
|
||||
@@ -235,8 +237,8 @@
|
||||
"externalLdapTooltip": "Desde un directorio LDAP externo",
|
||||
"users": "Usuarios",
|
||||
"name": "Nombre",
|
||||
"newGroupAction": "Nuevo Grupo",
|
||||
"title": "Grupos"
|
||||
"title": "Grupos",
|
||||
"emptyPlaceholder": "No hay grupos aún"
|
||||
},
|
||||
"users": {
|
||||
"transferOwnershipTooltip": "Transferir Propiedad",
|
||||
@@ -254,11 +256,8 @@
|
||||
"user": "Usuario",
|
||||
"setGhostTooltip": "Suplantar",
|
||||
"invitationTooltip": "Invitar Usuario",
|
||||
"mailmanagerTooltip": "Este usuario puede administrar usuarios y buzones de correo",
|
||||
"count": "Total usuarios: {{ count }}"
|
||||
"mailmanagerTooltip": "Este usuario puede administrar usuarios y buzones de correo"
|
||||
},
|
||||
"newUserAction": "Nuevo Usuario",
|
||||
"title": "Directorio de Usuarios",
|
||||
"transferOwnershipDialog": {
|
||||
"description": "Esto hará que el usuario seleccionado sea el propietario y administrador de este Cloudron y eliminará los derechos de administrador del propietario actual.",
|
||||
"title": "¿Realmente quieres transferir la propiedad?",
|
||||
@@ -369,8 +368,6 @@
|
||||
"placeholder": "Dirección IP o Subred separada por líneas",
|
||||
"label": "Acceso Restringido"
|
||||
},
|
||||
"enabled": "Habilitado",
|
||||
"title": "Servidor de Directorio",
|
||||
"description": "Cloudron puede actuar como un servidor de directorio de usuarios central para aplicaciones externas.",
|
||||
"secret": {
|
||||
"label": "Vincular Contraseña",
|
||||
@@ -427,7 +424,7 @@
|
||||
"configure": "Configurar",
|
||||
"retentionPolicy": "Política de retención",
|
||||
"schedule": "Programar",
|
||||
"description": "Se crea una copia de seguridad completa del sistema según la programación especificada en la <a href=\"/#/settings\">Zona horaria del sistema</a>. Las copias de seguridad antiguas se eliminan según la Política de retención.",
|
||||
"description": "Se crea una copia de seguridad completa del sistema según la programación especificada en la <a href=\"/#/system-locale\">Zona horaria del sistema</a>. Las copias de seguridad antiguas se eliminan según la Política de retención.",
|
||||
"title": "Programación y retención"
|
||||
},
|
||||
"location": {
|
||||
@@ -475,7 +472,6 @@
|
||||
"localDirectory": "Directorio local para copias de seguridad",
|
||||
"mountPointDescription": "El punto de montaje debe configurarse manualmente. Consulta esta <a href=\"{{ providerDocsLink }}\" target=\"_blank\"> documentación </a>.",
|
||||
"mountPoint": "Punto de montaje",
|
||||
"noopNote": "Esta opción rompe la funcionalidad de copia de seguridad y restauración de Cloudron y solo debe usarse para realizar pruebas. Asegúrese de que se haya realizado una copia de seguridad completa del servidor utilizando medios alternativos.",
|
||||
"provider": "Proveedor de almacenamiento",
|
||||
"title": "Configurar el almacenamiento de la Copia de Seguridad",
|
||||
"password": "Contraseña",
|
||||
@@ -491,7 +487,8 @@
|
||||
"cifsSealSupport": "Utiliza la encriptación seal. Requiere al menos SMB v3",
|
||||
"chown": "El sistema de archivos remoto admite chown",
|
||||
"encryptedFilenames": "Nombres de archivos encriptados",
|
||||
"encryptFilenames": "Encriptar nombres de archivo"
|
||||
"encryptFilenames": "Encriptar nombres de archivo",
|
||||
"preserveAttributesLabel": "Conservar atributos de archivo"
|
||||
},
|
||||
"configureBackupSchedule": {
|
||||
"retentionPolicy": "Política de Retención",
|
||||
@@ -529,6 +526,23 @@
|
||||
"tooltip": "Esto también conservará el correo y las copias de seguridad de la aplicación {{ appsLength }}."
|
||||
},
|
||||
"remotePath": "Ruta remota"
|
||||
},
|
||||
"restoreArchiveDialog": {
|
||||
"restoreAction": "Restaurar",
|
||||
"restoreActionOverwrite": "Restaurar y sobrescribir DNS",
|
||||
"title": "Restaurar desde archivo",
|
||||
"description": "Esto instalará {{appId}} en la ubicación especificada con copia de seguridad de {{creationTime}}."
|
||||
},
|
||||
"archives": {
|
||||
"title": "Archivo de aplicación",
|
||||
"info": "Información"
|
||||
},
|
||||
"deleteArchiveDialog": {
|
||||
"description": "Después de la eliminación, el archivo se limpiará según la política de copia de seguridad.",
|
||||
"title": "Eliminar archivo de {{appTitle}} ({{fqdn}})"
|
||||
},
|
||||
"deleteArchive": {
|
||||
"deleteAction": "Borrar"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -542,7 +556,8 @@
|
||||
"errorNameRequired": "Se requiere un nombre",
|
||||
"name": "Nombre del Token API",
|
||||
"title": "Crear Token API",
|
||||
"access": "Acceso API"
|
||||
"access": "Acceso API",
|
||||
"allowedIpRanges": "Rango(s) de IP permitido(s)"
|
||||
},
|
||||
"createAppPassword": {
|
||||
"generatePassword": "Generar contraseña",
|
||||
@@ -587,16 +602,16 @@
|
||||
"expiresAt": "Expira el",
|
||||
"name": "Nombre",
|
||||
"title": "Tokens API",
|
||||
"newApiToken": "Nuevo Token API",
|
||||
"neverUsed": "nunca",
|
||||
"lastUsed": "Última utilizada",
|
||||
"scope": "Alcance",
|
||||
"readonly": "Solo lectura",
|
||||
"readwrite": "Lectura y Escritura"
|
||||
"readwrite": "Lectura y Escritura",
|
||||
"allowedIpRangesPlaceholder": "IP o subredes separadas por comas",
|
||||
"allowedIpRanges": "IPs permitidas"
|
||||
},
|
||||
"appPasswords": {
|
||||
"deletePasswordTooltip": "Borrar Contraseña",
|
||||
"newPassword": "Nueva Contraseña",
|
||||
"description": "Las contraseñas de aplicaciones son una medida de seguridad para proteger su cuenta de usuario de Cloudron. Si necesita acceder a una aplicación de Cloudron desde una aplicación móvil o cliente que no sea de confianza, puede iniciar sesión con su nombre de usuario y la contraseña alternativa generada aquí.",
|
||||
"noPasswordsPlaceholder": "No se crearon contraseñas para la Aplicación",
|
||||
"name": "Nombre",
|
||||
@@ -643,7 +658,13 @@
|
||||
"changeBackgroundImage": {
|
||||
"title": "Establecer imagen de fondo"
|
||||
},
|
||||
"enable2FANotAvailable": "No disponible para usuarios de una fuente de autentificación externa"
|
||||
"enable2FANotAvailable": "No disponible para usuarios de una fuente de autentificación externa",
|
||||
"removeAppPassword": {
|
||||
"title": "¿Seguro que quieres eliminar la contraseña {{ name }}?"
|
||||
},
|
||||
"removeApiToken": {
|
||||
"title": "¿Realmente quieres eliminar el token {{ name }}?"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"eventlog": {
|
||||
@@ -651,10 +672,8 @@
|
||||
"type": {
|
||||
"spamFilterTrainedInfo": "Filtro de spam entrenado con contenido del buzón",
|
||||
"deniedInfo": "Conexión denegada",
|
||||
"deliveredInfo": "Correo entregado",
|
||||
"receivedInfo": "Guardado",
|
||||
"outboundInfo": "En cola para entrega",
|
||||
"inboundInfo": "Recibido",
|
||||
"outboundInfo": "En cola para entrega saliente",
|
||||
"inboundInfo": "En cola para entrega entrante",
|
||||
"deferredInfo": "Error de entrega, se volverá a intentar en {{ delay }}s.",
|
||||
"bounceInfo": "Rebote de envíos",
|
||||
"spamFilterTrained": "Filtro de spam entrenado",
|
||||
@@ -666,7 +685,9 @@
|
||||
"outgoing": "Saliente",
|
||||
"overQuotaInfo": "El buzón {{ mailbox }} está {{ quotaPercent }}% lleno",
|
||||
"underQuotaInfo": "El buzón {{ mailbox }} ha caído por debajo del {{ quotaPercent }}% de cuota",
|
||||
"quota": "Cuota de buzón"
|
||||
"quota": "Cuota de buzón",
|
||||
"savedInfo": "Guardado",
|
||||
"sentInfo": "Enviado"
|
||||
},
|
||||
"empty": "El Registro de Eventos está vacío.",
|
||||
"details": "Detalles",
|
||||
@@ -681,7 +702,7 @@
|
||||
"solrRunning": "En funcionamiento",
|
||||
"solrDisabled": "Deshabilitado",
|
||||
"solrEnabled": "Habilitado",
|
||||
"solrFts": "Búsqueda de texto completo (Solr)",
|
||||
"solrFts": "Búsqueda de texto completo",
|
||||
"changeDomainProgress": "Cambiar Email del dominio:",
|
||||
"spamFilterOverview": "{{ blacklistCount }} dirección(es) en la lista negra.",
|
||||
"spamFilter": "Filtro de Spam",
|
||||
@@ -706,9 +727,9 @@
|
||||
"typeFilterHeader": "Todos los Eventos",
|
||||
"solrConfig": {
|
||||
"notEnoughMemory": "Asigne al menos 3 GB al servicio de correo para habilitar solr.",
|
||||
"enableSolrCheckbox": "Habilitar la búsqueda de texto completo usando Solr",
|
||||
"description": "Solr se puede utilizar para proporcionar una búsqueda rápida de texto completo para correos electrónicos. Solr solo se ejecuta si al <a href=\"/#/services\" target=\"_blank\"> servicio de correo </a> se le ha asignado al menos 3 GB de RAM.",
|
||||
"title": "Búsqueda de texto completo (Solr)"
|
||||
"enableSolrCheckbox": "Habilitar búsqueda de texto completo",
|
||||
"description": "Solr y Tika se pueden utilizar para ofrecer una búsqueda rápida de texto completo en correos electrónicos y archivos adjuntos. Solr solo se ejecuta si al <a href=\"/#/services\" target=\"_blank\">servicio de correo</a> se le han asignado al menos 3 GB de RAM.",
|
||||
"title": "Búsqueda de texto completo"
|
||||
},
|
||||
"testMailDialog": {
|
||||
"sendAction": "Enviar",
|
||||
@@ -731,7 +752,7 @@
|
||||
"title": "Cambiar el tamaño máximo de email"
|
||||
},
|
||||
"changeDomainDialog": {
|
||||
"manualInfo": "Agrega un registro A manualmente para el {{dominio}} a la IP pública de este Cloudron",
|
||||
"manualInfo": "Configurar manualmente los registros DNS A (IPv4) y AAAA (IPv6) para <b>{{ domain }}</b> que apuntan a este servidor",
|
||||
"locationPlaceholder": "Dejar vacío para usar el dominio desnudo",
|
||||
"location": "Ubicación",
|
||||
"description": "Esto moverá el servidor IMAP y SMTP a la ubicación especificada.",
|
||||
@@ -780,7 +801,7 @@
|
||||
"title": "Pie de página"
|
||||
},
|
||||
"cloudronName": "Nombre de Cloudron",
|
||||
"title": "Marca",
|
||||
"title": "Apariencia",
|
||||
"backgroundImage": "Imagen de fondo de la página de inicio de sesión",
|
||||
"clearBackgroundImage": "Limpiar"
|
||||
},
|
||||
@@ -856,9 +877,7 @@
|
||||
"settings": {
|
||||
"appstoreAccount": {
|
||||
"title": "Cuenta Cloudron.io",
|
||||
"email": "Email de la Cuenta",
|
||||
"subscriptionEndsAt": "Cancelado y finaliza el",
|
||||
"subscriptionSetupAction": "Actualiza a Premium",
|
||||
"subscriptionReactivateAction": "Reactivar Suscripción",
|
||||
"setupAction": "Configurar Cuenta",
|
||||
"subscription": "Suscripción",
|
||||
@@ -867,7 +886,7 @@
|
||||
"description": "Se utiliza una cuenta de Cloudron.io para acceder a la App Store y administrar su suscripción.",
|
||||
"emailNotVerified": "Correo aún no verificado"
|
||||
},
|
||||
"title": "Ajustes",
|
||||
"title": "Sistema",
|
||||
"updateScheduleDialog": {
|
||||
"description": "Seleccione los días y horas durante los cuales Cloudron aplicará actualizaciones automáticas de la plataforma y la aplicación. Tenga cuidado de no superponer esta programación con la <a href=\"/#/backups\"> programación de copias de seguridad </a>.",
|
||||
"hours": "Horas",
|
||||
@@ -899,20 +918,11 @@
|
||||
},
|
||||
"privateDockerRegistry": {
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago.",
|
||||
"server": "Dirección del Servidor",
|
||||
"username": "Nombre de Usuario",
|
||||
"configureAction": "Configurar Registro",
|
||||
"setupSubscriptionAction": "Configura tu Suscripción Ahora",
|
||||
"usernameNotSet": "No configurado",
|
||||
"title": "Registro Privado de Docker",
|
||||
"description": "Cloudron puede extraer e instalar <a href=\"{{ customAppsLink }}\" target=\"_blank\"> aplicaciones personalizadas </a> desde un registro de Docker privado.",
|
||||
"serverNotSet": "Sin configurar"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"title": "Configuración del registro privado",
|
||||
"email": "Email (Opcional)",
|
||||
"passwordToken": "Contraseña / Token"
|
||||
},
|
||||
"registryConfig": {
|
||||
"provider": "Proveedor de registro de Docker",
|
||||
"providerOther": "Otra",
|
||||
@@ -973,7 +983,7 @@
|
||||
"addDescription": "Agregar un dominio le permite instalar aplicaciones en subdominios de este dominio. La configuración de correo electrónico para el dominio se puede configurar en la vista de correo electrónico.",
|
||||
"namecheapUsername": "Usuario de Namecheap",
|
||||
"namecheapInfo": "La IP del servidor debe estar incluida en la lista de permisos para esta clave de API.",
|
||||
"wildcardInfo": "Configura <i>los registros A</i> para <b>*.{{ domain }}</b> y <b>{{ domain }}</b> con la IP de este servidor.",
|
||||
"wildcardInfo": "Configurar manualmente los registros DNS A (IPv4) y AAAA (IPv6) para <b>*.{{ domain }}.</b> y <b>{{ domain }}.</b> que apuntan a este servidor",
|
||||
"matrixHostname": "Ubicación del Servidor Matrix",
|
||||
"fallbackCertInfo": "Los certificados se obtienen y renuevan automáticamente desde <a href=\"https://letsencrypt.org/\" target=\"_blank\"> Let's Encrypt </a>. Consulta el límite de frecuencia actual <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\"> aquí </a>.\nEste certificado se utilizará en caso de que falle el certificado de Let's Encrypt. Si no se proporciona, se utilizará como respaldo un certificado autofirmado generado automáticamente.",
|
||||
"fallbackCertCustomCertInfo": "Este <a href=\"{{ customCertLink }}\" target=\"_blank\"> certificado wildcard </a> se utilizará para todas las aplicaciones de este dominio. Si no se proporciona, se generará automáticamente un certificado autofirmado.",
|
||||
@@ -990,7 +1000,13 @@
|
||||
"ovhConsumerKey": "Clave del consumidor",
|
||||
"ovhAppKey": "Clave de Aplicación",
|
||||
"ovhAppSecret": "Clave Secreta Aplicación",
|
||||
"deSecToken": "Token deSEC"
|
||||
"deSecToken": "Token deSEC",
|
||||
"gandiTokenTypeApiKey": "Clave API (obsoleta)",
|
||||
"gandiTokenType": "Tipo de Token",
|
||||
"gandiTokenTypePAT": "Token de acceso personal (PAT)",
|
||||
"inwxUsername": "Nombre de usuario",
|
||||
"inwxPassword": "Contraseña",
|
||||
"customNameservers": "El dominio utiliza servidores de nombres personalizados (vanity)"
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"setupAction": "Configura tu suscripción",
|
||||
@@ -1007,7 +1023,6 @@
|
||||
"tooltipEdit": "Editar Dominio",
|
||||
"provider": "Proveedor",
|
||||
"domain": "Dominio",
|
||||
"addDomain": "Añadir Dominio",
|
||||
"syncDns": {
|
||||
"showLogsAction": "Mostrar Registros",
|
||||
"title": "Sincronizar DNS",
|
||||
@@ -1036,17 +1051,11 @@
|
||||
"firstTimeCollapseHeader": "Instrucciones de ajustes de primera vez",
|
||||
"appDocsUrl": "Consulta la <a target=\"_blank\" href=\"{{ docsUrl }}\"> {{title}} documentación </a> para obtener información útil y temas comunes sobre esta aplicación. Si necesita más ayuda, consulta la <a target=\"_blank\" href=\"{{ forumUrl }}\"> {{title}} sección del foro </a> de Cloudron.",
|
||||
"package": "Paquete",
|
||||
"checklist": "Lista de verificación del administrador"
|
||||
"checklist": "Lista de verificación del administrador",
|
||||
"checklistShow": "Mostrar lista de verificación",
|
||||
"checklistHide": "Ocultar lista de verificación"
|
||||
},
|
||||
"updates": {
|
||||
"auto": {
|
||||
"enableAction": "Habilitar actualizaciones automáticas",
|
||||
"disableAction": "Deshabilitar la actualización automática",
|
||||
"disabled": "Las Actualizaciones Automáticas están deshabilitadas actualmente.",
|
||||
"enabled": "Las Actualizaciones Automáticas están habilitadas actualmente.",
|
||||
"description": "Cloudron comprueba periódicamente la <a href=\"{{ appStoreLink }}\" target=\"_blank\">Tienda de Aplicaciones</a> en busca de actualizaciones.",
|
||||
"title": "Actualizaciones Automáticas"
|
||||
},
|
||||
"info": {
|
||||
"updateAvailableAction": "Actualización disponible",
|
||||
"customAppUpdateInfo": "La actualización automática no está disponible para aplicaciones personalizadas.",
|
||||
@@ -1068,7 +1077,7 @@
|
||||
"noMounts": "No se ha montado ningún volumen.",
|
||||
"volume": "Volumen",
|
||||
"saveAction": "Guardar",
|
||||
"title": "Montajes",
|
||||
"title": "Montajes de volumen",
|
||||
"readOnly": "Solo lectura",
|
||||
"permissions": {
|
||||
"label": "Permisos",
|
||||
@@ -1092,7 +1101,9 @@
|
||||
"12h": "12 horas",
|
||||
"30d": "30 días",
|
||||
"7d": "7 días",
|
||||
"6h": "6 horas"
|
||||
"6h": "6 horas",
|
||||
"live": "En vivo",
|
||||
"1h": "1 hora"
|
||||
},
|
||||
"memoryTitle": "Memoria (RAM + Swap) en Mb",
|
||||
"diskTitle": "Uso del disco",
|
||||
@@ -1114,7 +1125,6 @@
|
||||
"cloneTooltip": "Clonar desde esta Copia de Seguridad",
|
||||
"downloadConfigTooltip": "Descarga Configuración de la Copia de Seguridad",
|
||||
"time": "Creado en",
|
||||
"packageVersion": "Versión del Paquete",
|
||||
"title": "Backups",
|
||||
"description": "Las copias de seguridad son instantáneas completas de la aplicación. Puede utilizar copias de seguridad de la aplicación para restaurar o clonar esta aplicación.",
|
||||
"downloadBackupTooltip": "Descargar Copia de Seguridad"
|
||||
@@ -1151,7 +1161,7 @@
|
||||
"title": "Correo DESDE la dirección",
|
||||
"disableDescription": "La configuración de entrega de correo de la aplicación es independiente. Puedes configurarla dentro de la aplicación.",
|
||||
"description2": "Cuando está habilitada, la aplicación está configurada para enviar correos electrónicos a través del servidor de correo interno usando esta dirección. El servidor de correo interno utilizará la configuración de {{domain}} <a href=\"{{ domainConfigLink }}\"> correo electrónico saliente </a> para enviar correo. Cuando está deshabilitado, puede configurar los ajustes de correo electrónico dentro de la aplicación.",
|
||||
"mailboxPlaceholder": "Dejar vacío para usar la plataforma predeterminada",
|
||||
"mailboxPlaceholder": "Nombre del buzón",
|
||||
"disable": "No configurar la configuración de entrega de correo de la aplicación",
|
||||
"enableDescription": "La aplicación está configurada para enviar correos electrónicos utilizando la dirección que aparece a continuación y la configuración de <a href=\"{{ domainConfigLink }}\"> correo electrónico saliente </a> de {{domain}}.",
|
||||
"description": "Esto establece la dirección desde la que esta aplicación envía el correo electrónico. Esta aplicación ya está configurada para enviar correo mediante la configuración de {{domain}} <a href=\\\"{{ domainConfigLink }}\\\"> correo electrónico saliente </a>.",
|
||||
@@ -1214,7 +1224,8 @@
|
||||
"redirectionsPlaceholder": "Dejar vacío para usar el dominio desnudo",
|
||||
"redirections": "Redirecciones",
|
||||
"locationPlaceholder": "Dejar vacío para usar el dominio desnudo",
|
||||
"location": "Ubicación"
|
||||
"location": "Ubicación",
|
||||
"dnsoverwrite": "Ya existen algunos registros DNS. Acepta para sobrescribirlos."
|
||||
},
|
||||
"display": {
|
||||
"saveAction": "Guardar",
|
||||
@@ -1238,7 +1249,7 @@
|
||||
"backAction": "Volver a Mis Aplicaciones",
|
||||
"adminPageAction": "Página de administrador",
|
||||
"uninstallDialog": {
|
||||
"description": "Esto desinstalará inmediatamente <b>{{ app }}</b> y borrará todos sus datos.",
|
||||
"description": "Esto desinstalará {{ app }} y borrará todos sus datos.",
|
||||
"title": "Desinstalar {{ app }}",
|
||||
"uninstallAction": "Desinstalar"
|
||||
},
|
||||
@@ -1259,18 +1270,20 @@
|
||||
"warning": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una restauración.",
|
||||
"title": "Restaurar {{ app }}",
|
||||
"description": "Esto restaurará esta aplicación con los datos de {{creationTime}}.",
|
||||
"restoreAction": "Restaurar"
|
||||
"restoreAction": "Restaurar",
|
||||
"cloneAction": "Clonar",
|
||||
"cloneActionOverwrite": "Clonar y sobreescribir DNS"
|
||||
},
|
||||
"uninstall": {
|
||||
"uninstall": {
|
||||
"uninstallAction": "Desinstalar",
|
||||
"title": "Desinstalar",
|
||||
"description": "Esto desinstalará la aplicación inmediatamente y eliminará tus datos. El sitio será inaccesible."
|
||||
"description": "Esto desinstalará la aplicación y eliminará sus datos. Las copias de seguridad se limpiarán según la política de copias de seguridad."
|
||||
},
|
||||
"startStop": {
|
||||
"title": "Arrancar / Parar",
|
||||
"startAction": "Arrancar Aplicación",
|
||||
"stopAction": "Parar Aplicación",
|
||||
"startAction": "Arrancar",
|
||||
"stopAction": "Parar",
|
||||
"description": "Las aplicaciones se pueden detener para conservar los recursos del servidor en lugar de desinstalarlas. Las futuras copias de seguridad de la aplicación no incluirán ningún cambio en la aplicación entre ahora y la copia de seguridad de la aplicación más reciente. Por este motivo, se recomienda activar una copia de seguridad antes de detener la aplicación."
|
||||
}
|
||||
},
|
||||
@@ -1310,9 +1323,7 @@
|
||||
"repair": {
|
||||
"recovery": {
|
||||
"title": "Recuperación en caso de accidente",
|
||||
"restartAction": "Reiniciar Aplicación",
|
||||
"enableRecoveryModeAction": "Habilitar Modo de Recuperación",
|
||||
"disableRecoveryModeAction": "Deshabilitar Modo de Recuperación",
|
||||
"restartAction": "Reiniciar",
|
||||
"description": "Si la aplicación no responde, intenta reiniciarla. Si la aplicación se reinicia constantemente debido a un complemento roto o una configuración incorrecta, coloca la aplicación en modo de recuperación para acceder a la consola.\nUtiliza las siguientes <a href=\"{{ docsLink }}\" target=\"_blank\"> instrucciones </a> para volver a ejecutar la aplicación."
|
||||
},
|
||||
"taskError": {
|
||||
@@ -1360,19 +1371,34 @@
|
||||
"turn": {
|
||||
"title": "Configuración de TURN",
|
||||
"enable": "Configura la aplicación para utilizar el servidor TURN integrado",
|
||||
"disable": "No configures los ajustes de la aplicación TURN. Su configuración se deja como está. Puedes hacer los ajustes dentro de la aplicación."
|
||||
"disable": "No configures los ajustes de la aplicación TURN. Su configuración se deja como está. Puedes hacer los ajustes dentro de la aplicación.",
|
||||
"info": "Habilita esta opción para configurar la aplicación para que use el servidor TURN integrado. Al deshabilitarla, la configuración de TURN de la aplicación se conserva."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Configuración de Redis",
|
||||
"enable": "Configura la aplicación para usar Redis",
|
||||
"disable": "Deshabilitar Redis"
|
||||
"disable": "Deshabilitar Redis",
|
||||
"info": "Si está habilitado, la aplicación usará el servicio Redis integrado. Si está deshabilitado, la configuración de Redis de la aplicación no se modifica."
|
||||
},
|
||||
"infoTabTitle": "Información",
|
||||
"info": {
|
||||
"notes": {
|
||||
"title": "Notas del Administrador"
|
||||
}
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"action": "Archivo",
|
||||
"description": "La última copia de seguridad de la aplicación se agregará al <a href=\"/#/backups\">Archivo de aplicaciones</a>. La aplicación se desinstalará, pero se podrá restaurar desde la Vista de copias de seguridad. Las demás copias de seguridad se limpiarán según la política de copias de seguridad.",
|
||||
"noBackup": "Esta aplicación no tiene copia de seguridad. Para archivarla, es necesario tener una copia de seguridad reciente.",
|
||||
"latestBackupInfo": "La última copia de seguridad se creó el {{date}}.",
|
||||
"title": "Archivo"
|
||||
},
|
||||
"archiveDialog": {
|
||||
"title": "Archivo {{app}}",
|
||||
"description": "Esto desinstalará la aplicación y colocará la última copia de seguridad de la aplicación creada el {{date}} en el Archivo de aplicaciones."
|
||||
},
|
||||
"configureTooltip": "Configurar",
|
||||
"updateAvailableTooltip": "Actualización disponible"
|
||||
},
|
||||
"lang": {
|
||||
"zh_Hans": "Chino (simple)",
|
||||
@@ -1391,7 +1417,6 @@
|
||||
"id": "Indonesio"
|
||||
},
|
||||
"system": {
|
||||
"title": "Información del Sistema",
|
||||
"cpuUsage": {
|
||||
"graphTitle": "Porcentaje",
|
||||
"title": "Uso de CPU",
|
||||
@@ -1418,12 +1443,14 @@
|
||||
"memory": "Memoria",
|
||||
"uptime": "Tiempo de actividad",
|
||||
"activationTime": "Tiempo de creación de Cloudron",
|
||||
"platformVersion": "Versión de plataforma",
|
||||
"product": "Producto",
|
||||
"vendor": "Vendedor"
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Gráficos"
|
||||
},
|
||||
"locale": {
|
||||
"title": "Configuración regional"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -1448,7 +1475,6 @@
|
||||
"type": "Tipo",
|
||||
"subscriptionRequired": "Los tickets de soporte solo están disponibles en planes de pago.",
|
||||
"title": "Tiquet",
|
||||
"subscriptionRequiredDescription": "Puedes encontrar respuestas en nuestra <a href=\"{{ supportViewLink }}\" target=\"_blank\">documentación</a> o pregunta en el <a href=\"{{ forumLink }}\" target=\"_blank\">Foro</a>.",
|
||||
"emailInfo": "(El email de suscripción es {{ email }})",
|
||||
"sshCheckbox": "Permitir que los ingenieros de soporte se conecten a este servidor a través de SSH",
|
||||
"emailPlaceholder": "Si es necesario, proporciona una dirección de correo electrónico diferente de la anterior para contactarte",
|
||||
@@ -1486,7 +1512,6 @@
|
||||
"openFileManagerActionTooltip": "Abrir Gestor de Archivos",
|
||||
"name": "Nombre",
|
||||
"hostPath": "Objetivo",
|
||||
"addVolumeAction": "Añade un Volumen",
|
||||
"title": "Volúmenes",
|
||||
"description": "Los volúmenes son sistemas de archivos locales o remotos. Se pueden usar como el almacenamiento de datos principal de una aplicación o como una ubicación de almacenamiento compartida entre aplicaciones.",
|
||||
"localDirectory": "Directorio Local",
|
||||
@@ -1553,7 +1578,7 @@
|
||||
"closeWarning": "No refresques la página hasta que la subida haya terminado."
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "¿Realmente quieres eliminar lo siguiente?"
|
||||
"reallyDelete": "¿Realmente quieres eliminar?"
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Extrayendo {{ fileName }}",
|
||||
@@ -1620,16 +1645,14 @@
|
||||
},
|
||||
"email": {
|
||||
"signature": {
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Configura tu Suscripción Ahora</a>",
|
||||
"plainTextFormat": "Formato del texto",
|
||||
"htmlFormat": "Formato HTML (Opcional)",
|
||||
"htmlFormat": "Formato HTML",
|
||||
"saveAction": "Guardar",
|
||||
"title": "Firma",
|
||||
"description": "El texto aquí se adjuntará a todos los correos electrónicos que se envíen desde este dominio."
|
||||
},
|
||||
"incoming": {
|
||||
"catchall": {
|
||||
"subscriptionRequired": "Esta funcionalidad solo está disponible en planes de pago. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Configura tu Suscripción Ahora</a>",
|
||||
"description": "Los correos electrónicos enviados a direcciones no existentes se reenviarán a los siguientes buzones de correo.",
|
||||
"title": "Atrapa todo",
|
||||
"saveAction": "Guardar"
|
||||
@@ -1666,8 +1689,6 @@
|
||||
"port": "Puerto",
|
||||
"tabTitle": "Buzones de correo",
|
||||
"incomingServerInfo": "Correo entrante (IMAP)",
|
||||
"enabled": "El servidor de correo electrónico de Cloudron está configurado para recibir correos electrónicos entrantes para este dominio.",
|
||||
"disabled": "El servidor de correo electrónico de Cloudron no recibirá correos electrónicos entrantes para este dominio.",
|
||||
"howToConnectDescription": "Utiliza la siguiente configuración para configurar los clientes de correo electrónico.",
|
||||
"incomingUserInfo": "Nombre de Usuario",
|
||||
"incomingPasswordInfo": "Contraseña",
|
||||
@@ -1714,12 +1735,10 @@
|
||||
"recordNotSet": "no establecido"
|
||||
},
|
||||
"smtpStatus": {
|
||||
"outboudRelay": "Saliente SMTP (Retransmitido)",
|
||||
"notBlacklisted": "La IP de este servidor {{ ip }} <b> no </b> está en una lista de bloqueo.",
|
||||
"title": "Estado SMTP",
|
||||
"outboudDirect": "Saliente SMTP (Directo)",
|
||||
"blacklistCheck": "Comprobación de la lista negra de direcciones IP",
|
||||
"blacklisted": "La IP de este servidor {{ip}} está en una lista de bloqueo."
|
||||
"blacklisted": "La IP de este servidor {{ip}} está en una lista de bloqueo.",
|
||||
"rblCheck": "Comprobación de lista negra de DNS",
|
||||
"outboundSmtp": "SMTP saliente"
|
||||
},
|
||||
"enableEmailDialog": {
|
||||
"noProviderInfo": "No se ha configurado ningún proveedor de DNS. Los registros DNS enumerados en la pestaña Estado deben configurarse manualmente.",
|
||||
@@ -1819,7 +1838,22 @@
|
||||
"dismissTooltip": "Descartar",
|
||||
"title": "Notificaciones",
|
||||
"nonePending": "No hay notificaciones!",
|
||||
"markAllAsRead": "Marcar Todos como leídos"
|
||||
"markAllAsRead": "Marcar Todos como leídos",
|
||||
"settings": {
|
||||
"rebootRequired": "Es necesario reiniciar el servidor",
|
||||
"cloudronUpdateFailed": "La actualización de Cloudron ha fallado",
|
||||
"title": "Configuración de notificaciones",
|
||||
"appOutOfMemory": "La aplicación se quedó sin memoria",
|
||||
"certificateRenewalFailed": "La renovación del certificado falló",
|
||||
"appUp": "La aplicación está nuevamente en línea",
|
||||
"backupFailed": "La copia de seguridad falló",
|
||||
"appDown": "La aplicación no funciona",
|
||||
"diskSpace": "Poco espacio en disco"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "Se enviará un correo electrónico para los eventos seleccionados a su dirección de correo electrónico principal."
|
||||
},
|
||||
"allCaughtUp": "Todos atrapados"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -1912,7 +1946,7 @@
|
||||
"errorIncorrectCredentials": "Nombre de usuario o contraseña incorrectos",
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"2faToken": "Token 2FA (si está habilitado)",
|
||||
"2faToken": "Token 2FA",
|
||||
"signInAction": "Iniciar Sesión",
|
||||
"resetPasswordAction": "Resetear contraseña",
|
||||
"errorIncorrect2FAToken": "El token 2FA es inválido",
|
||||
@@ -1931,31 +1965,31 @@
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
"description": "Las aplicaciones pueden acceder a <a href=\"/#/volumes\">volúmenes</a> montados a través del directorio <code>/media/{volume name}</code>. Estos datos no están incluidos en la copia de seguridad de la aplicación."
|
||||
"description": "Las aplicaciones pueden acceder a <a href=\"/#/volumes\">volúmenes</a> montados a través del directorio <code>/media/(volume name)</code>. Estos datos no están incluidos en la copia de seguridad de la aplicación."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Añadir Cliente",
|
||||
"description": "Agrega una nueva configuración de cliente de conexión de OpenID.",
|
||||
"createAction": "Crear"
|
||||
"title": "Añadir Cliente OIDC",
|
||||
"description": "Ingresar nueva configuración de cliente OIDC",
|
||||
"createAction": "Añadir"
|
||||
},
|
||||
"client": {
|
||||
"name": "Nombre",
|
||||
"id": "ID de cliente",
|
||||
"secret": "Secreto de cliente",
|
||||
"signingAlgorithm": "Algoritmo de firma",
|
||||
"loginRedirectUri": "URL de devolución de llamada de inicio de sesión (separadas por comas si hay más de una)",
|
||||
"loginRedirectUri": "URLs de devolución de llamada de inicio de sesión (separadas por comas)",
|
||||
"logoutRedirectUri": "URL de devolución de llamada de cierre de sesión (opcional)"
|
||||
},
|
||||
"title": "Proveedor de conexión OpenID",
|
||||
"title": "Proveedor de OpenID",
|
||||
"description": "Cloudron puede actuar como proveedor de OpenID Connect para aplicaciones internas y servicios externos.",
|
||||
"editClientDialog": {
|
||||
"title": "Editar cliente {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "¿Realmente quieres borrar el cliente {{ client }}?",
|
||||
"description": "Esto desconectará todas las aplicaciones OpenID externas de este Cloudron que utilicen este ID de cliente."
|
||||
"description": "Si eliminas este cliente OIDC, se invalidarán todos los tokens de acceso. Las aplicaciones que utilicen este cliente OIDC ya no podrán autentificarse."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "URL de descubrimiento",
|
||||
@@ -1964,12 +1998,12 @@
|
||||
"keysEndpoint": "Punto final de claves",
|
||||
"tokenEndpoint": "Punto final del Token",
|
||||
"authEndpoint": "Punto final de autenticación"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clientes",
|
||||
"newClient": "Nuevo cliente",
|
||||
"empty": "No hay clientes aún"
|
||||
}
|
||||
},
|
||||
"automation": "Automatización"
|
||||
"automation": "Automatización",
|
||||
"userdirectory": {
|
||||
"settings": {
|
||||
"title": "Ajustes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@
|
||||
"saveAction": "Sauvegarde"
|
||||
},
|
||||
"users": {
|
||||
"title": "Annuaire des utilisateurs",
|
||||
"users": {
|
||||
"user": "Utilisateur",
|
||||
"notActivatedYetTooltip": "Utilisateur pas encore activé",
|
||||
@@ -112,15 +111,12 @@
|
||||
"externalLdapTooltip": "Depuis un annuaire LDAP externe",
|
||||
"setGhostTooltip": "Emprunter l'identité",
|
||||
"invitationTooltip": "Envoyer une invitation à l'utilisateur",
|
||||
"mailmanagerTooltip": "Cet utilisateur peut gérer les utilisateurs et les boîtes mail",
|
||||
"count": "Total des utilisateurs : {{ count }}"
|
||||
"mailmanagerTooltip": "Cet utilisateur peut gérer les utilisateurs et les boîtes mail"
|
||||
},
|
||||
"newUserAction": "Nouvel utilisateur",
|
||||
"groups": {
|
||||
"name": "Nom",
|
||||
"title": "Groupes",
|
||||
"users": "Utilisateurs",
|
||||
"newGroupAction": "Nouveau groupe",
|
||||
"externalLdapTooltip": "Depuis un annuaire LDAP externe"
|
||||
},
|
||||
"settings": {
|
||||
@@ -279,13 +275,11 @@
|
||||
"url": "URL du serveur"
|
||||
},
|
||||
"description": "Cloudron peut agir en tant que serveur d'annuaire d'utilisateurs central pour les applications externes.",
|
||||
"enabled": "Activé",
|
||||
"ipRestriction": {
|
||||
"description": "Le serveur d'annuaire peut être limité à certaines adresses IP ou à des plages spécifiques.",
|
||||
"placeholder": "Adresse IP séparée par ligne ou sous-réseau",
|
||||
"label": "Accès restreint"
|
||||
},
|
||||
"title": "Serveur d'annuaire",
|
||||
"cloudflarePortWarning": "Le proxy Cloudflare doit être désactivé sur le domaine du tableau de bord pour accéder au service LDAP"
|
||||
},
|
||||
"userImportDialog": {
|
||||
@@ -340,7 +334,6 @@
|
||||
"appPasswords": {
|
||||
"app": "Application",
|
||||
"deletePasswordTooltip": "Supprimer mot de passe",
|
||||
"newPassword": "Nouveau mot de passe",
|
||||
"name": "Nom",
|
||||
"noPasswordsPlaceholder": "Aucun mot de passe d'application créé",
|
||||
"title": "Mots de passe d'application",
|
||||
@@ -400,7 +393,6 @@
|
||||
"name": "Nom",
|
||||
"noTokensPlaceholder": "Pas de jeton API créé",
|
||||
"revokeTokenTooltip": "Révoquer jeton",
|
||||
"newApiToken": "Nouveau jeton API",
|
||||
"title": "Jetons API",
|
||||
"description": "Utilisez ces jetons d'accès personnels pour vous authentifier avec <a target=\"_blank\" href=\"{{ apiDocsLink }}\">l'API Cloudron</a>",
|
||||
"neverUsed": "jamais",
|
||||
@@ -461,7 +453,7 @@
|
||||
},
|
||||
"configureBackupStorage": {
|
||||
"memoryLimit": "Limite de la mémoire allouée",
|
||||
"encryptionPassword": "Clé de chiffrement (optionnel)",
|
||||
"encryptionPassword": "Clé de chiffrement",
|
||||
"copyConcurrency": "Simultanéité des copies",
|
||||
"encryptionPasswordRepeat": "Répéter le mot de passe",
|
||||
"hardlinksLabel": "Utiliser des liens durs",
|
||||
@@ -471,7 +463,6 @@
|
||||
"uploadConcurrencyDescription": "Nombre de fichiers pouvant être envoyés simultanément au cours d'une sauvegarde",
|
||||
"downloadConcurrencyDescription": "Nombre de fichiers pouvant être reçus simultanément au cours d'une restauration",
|
||||
"copyConcurrencyDescription": "Nombre de fichiers à distance pouvant être copiés au cours d'une sauvegarde",
|
||||
"noopNote": "Cette option neutralise les fonctionnalités de sauvegarde et de restauration de Cloudron et ne doit être utilisée qu'à des fins de test. Veuillez vous assurer qu'une sauvegarde complète du système a été effectuée par d'autres moyens.",
|
||||
"prefix": "Préfixe",
|
||||
"region": "Région",
|
||||
"s3AccessKeyId": "ID de clé d'accès",
|
||||
@@ -572,9 +563,7 @@
|
||||
"outgoing": "Sortant",
|
||||
"deniedInfo": "Accès refusé",
|
||||
"deferredInfo": "Échec de l'envoi. Nouvelle tentative dans {{ delay }}s.",
|
||||
"deliveredInfo": "Email envoyé",
|
||||
"inboundInfo": "Reçu",
|
||||
"receivedInfo": "Sauvegardé",
|
||||
"outboundInfo": "Envoi en attente",
|
||||
"deferred": "Reporté",
|
||||
"incoming": "Entrant",
|
||||
@@ -733,20 +722,14 @@
|
||||
"setupSubscriptionAction": "Paramétrer mon abonnement maintenant",
|
||||
"configureAction": "Paramétrer le registre",
|
||||
"usernameNotSet": "Non défini",
|
||||
"username": "Nom d'utilisateur",
|
||||
"server": "Adresse du serveur",
|
||||
"description": "Cloudron peut extraire et installer des <a href=\"{{ customAppsLink }}\" target=\"_blank\">applications personnalisées</a> depuis un registre Docker privé.",
|
||||
"title": "Registre Docker privé",
|
||||
"serverNotSet": "Pas encore défini"
|
||||
},
|
||||
"appstoreAccount": {
|
||||
"subscriptionSetupAction": "Passer à la version premium",
|
||||
"subscriptionReactivateAction": "Réactiver l'abonnement",
|
||||
"subscriptionChangeAction": "Modifier l'abonnement",
|
||||
"subscriptionEndsAt": "Prend fin le",
|
||||
"cloudronId": "ID Cloudron",
|
||||
"subscription": "Abonnement",
|
||||
"email": "Adresse email du compte",
|
||||
"setupAction": "Créer un compte",
|
||||
"description": "Un compte Cloudron.io permet d'accéder à l'App Store et de gérer votre abonnement.",
|
||||
"title": "Compte Cloudron.io",
|
||||
@@ -779,11 +762,6 @@
|
||||
"disableCheckbox": "Désactiver les mises à jour automatiques",
|
||||
"title": "Planification des mises à jour automatiques"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"passwordToken": "Mot de passe / Jeton",
|
||||
"email": "Adresse email (optionnel)",
|
||||
"title": "Paramétrage du registre privé"
|
||||
},
|
||||
"updates": {
|
||||
"stopUpdateAction": "Interrompre la mise à jour",
|
||||
"updateAvailableAction": "Mise à jour disponible",
|
||||
@@ -823,7 +801,6 @@
|
||||
"typeBug": "Rapport d'incident",
|
||||
"typeApp": "Problème avec une application",
|
||||
"type": "Type",
|
||||
"subscriptionRequiredDescription": "Vous devriez trouver votre réponse dans notre <a href=\"{{ supportViewLink }}\" target=\"_blank\">documentation</a>, vous pouvez également poser votre question sur le <a href=\"{{ forumLink }}\" target=\"_blank\">forum</a>.",
|
||||
"title": "Ticket",
|
||||
"emailVerifyAction": "Confirmer maintenant",
|
||||
"emailNotVerified": "L'adresse email de votre compte Cloudron.io {{ email }} n'a pas encore été confirmée. Veuillez la valider pour ouvrir des tickets d'incident.",
|
||||
@@ -942,14 +919,6 @@
|
||||
"title": "Informations sur l'application",
|
||||
"repository": "Dépot de paquets"
|
||||
},
|
||||
"auto": {
|
||||
"title": "Mises à jour automatiques",
|
||||
"enableAction": "Activer les mises à jour automatiques",
|
||||
"disableAction": "Désactiver les mises à jour automatiques",
|
||||
"disabled": "Les mises à jour automatiques sont actuellement désactivées.",
|
||||
"enabled": "Les mises à jour automatiques sont actuellement activées.",
|
||||
"description": "Cloudron vérifie régulièrement les mises à jour disponibles dans l'App Store. Si vous désactivez les mises à jour automatiques, veillez à les faire manuellement."
|
||||
},
|
||||
"noUpdates": "Aucune nouvelle mise à jour disponible"
|
||||
},
|
||||
"backupsTabTitle": "Sauvegardes",
|
||||
@@ -1022,8 +991,6 @@
|
||||
"repair": {
|
||||
"recovery": {
|
||||
"description": "Si l'application ne répond pas, essayez de redémarrer l'application. Si l'application redémarre sans arrêt à cause d'un plugin défectueux ou d'une anomalie de paramétrage, mettez l'application en mode récupération pour avoir accès à la console. \nSuivez les <a href=\"{{ docsLink }}\" target=\"_blank\">instructions suivantes</a> pour faire fonctionner l'application à nouveau.",
|
||||
"disableRecoveryModeAction": "Désactiver le mode récupération",
|
||||
"enableRecoveryModeAction": "Activer le mode récupération",
|
||||
"restartAction": "Redémarrer l'application",
|
||||
"title": "Récupération après un crash"
|
||||
},
|
||||
@@ -1101,7 +1068,7 @@
|
||||
"title": "Domaine de collision"
|
||||
},
|
||||
"uninstallDialog": {
|
||||
"description": "Cette action entraînera la désinstallation immédiate de <b>{{ app }}</b> et la suppression de l'ensemble de ses données.",
|
||||
"description": "Cette action entraînera la désinstallation immédiate de {{ app }} et la suppression de l'ensemble de ses données.",
|
||||
"uninstallAction": "Désinstaller",
|
||||
"title": "Désinstaller {{ app }}"
|
||||
},
|
||||
@@ -1150,7 +1117,6 @@
|
||||
"cloneTooltip": "Cloner depuis cette sauvegarde",
|
||||
"downloadConfigTooltip": "Télécharger le fichier de configuration de la sauvegarde",
|
||||
"time": "Créée le",
|
||||
"packageVersion": "Version du package",
|
||||
"description": "Les sauvegardes sont des instantanés complets de l'application. Vous pouvez utiliser les sauvegardes pour restaurer ou cloner l'application.",
|
||||
"title": "Sauvegardes",
|
||||
"downloadBackupTooltip": "Télécharger la sauvegarde"
|
||||
@@ -1301,7 +1267,6 @@
|
||||
"openFileManagerActionTooltip": "Ouvrir le gestionnaire de fichiers",
|
||||
"removeVolumeActionTooltip": "Supprimer le volume",
|
||||
"hostPath": "Point de montage",
|
||||
"addVolumeAction": "Ajouter un volume",
|
||||
"title": "Volumes",
|
||||
"localDirectory": "Répertoire local",
|
||||
"tooltipEdit": "Modifier le Volume",
|
||||
@@ -1344,7 +1309,6 @@
|
||||
"title": "Abonnement nécessaire"
|
||||
},
|
||||
"signature": {
|
||||
"subscriptionRequired": "Cette fonctionnalité est uniquement disponible dans la version payante. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Paramétrer mon abonnement maintenant</a>",
|
||||
"description": "Le texte ci-dessous s'affichera dans tous les emails sortants de ce domaine.",
|
||||
"plainTextFormat": "Format texte",
|
||||
"saveAction": "Sauvegarder",
|
||||
@@ -1353,7 +1317,6 @@
|
||||
},
|
||||
"incoming": {
|
||||
"catchall": {
|
||||
"subscriptionRequired": "Cette fonctionnalité est uniquement disponible dans la version payante. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Paramétrer mon abonnement maintenant</a>",
|
||||
"saveAction": "Sauvegarder",
|
||||
"description": "Les emails envoyés à des adresses non existantes seront transférés aux adresses email suivantes.",
|
||||
"title": "Email collecteur"
|
||||
@@ -1390,8 +1353,6 @@
|
||||
"enableAction": "Activer",
|
||||
"disableAction": "Désactiver",
|
||||
"title": "Email entrant",
|
||||
"enabled": "Le serveur de messagerie Cloudron est configuré pour recevoir les e-mails entrants pour ce domaine.",
|
||||
"disabled": "Le serveur de messagerie Cloudron ne recevra pas les e-mails entrants pour ce domaine.",
|
||||
"howToConnectDescription": "Utilisez les paramètres ci-dessous pour configurer les clients de messagerie.",
|
||||
"incomingUserInfo": "Identifiant",
|
||||
"incomingPasswordInfo": "Mot de passe",
|
||||
@@ -1440,11 +1401,7 @@
|
||||
},
|
||||
"smtpStatus": {
|
||||
"notBlacklisted": "L'adresse IP de ce serveur {{ ip }} <b>n'est pas</b> sur liste noire.",
|
||||
"blacklisted": "L'adresse IP de ce serveur {{ ip }} est sur liste noire.",
|
||||
"blacklistCheck": "Vérification de la liste noire des adresses IP",
|
||||
"outboudRelay": "SMTP sortant (relais)",
|
||||
"outboudDirect": "SMTP sortant (direct)",
|
||||
"title": "État des SMTP"
|
||||
"blacklisted": "L'adresse IP de ce serveur {{ ip }} est sur liste noire."
|
||||
},
|
||||
"dnsStatus": {
|
||||
"recordNotSet": "non défini",
|
||||
@@ -1608,7 +1565,6 @@
|
||||
"tooltipEdit": "Modifier le domaine",
|
||||
"provider": "Fournisseur",
|
||||
"domain": "Domaine",
|
||||
"addDomain": "Ajouter un domaine",
|
||||
"title": "Domaines et Certificats",
|
||||
"domainWellKnown": {
|
||||
"title": "Emplacements Well-Known de {{ domain }}"
|
||||
@@ -1827,9 +1783,7 @@
|
||||
"diskSpeed": "Vitesse : {{ speed }} MB/sec",
|
||||
"volumeContent": "Ce disque est le volume <code>{{ name }}</code>"
|
||||
},
|
||||
"title": "Info système",
|
||||
"info": {
|
||||
"platformVersion": "Version de la Plate-forme",
|
||||
"vendor": "Vendeur",
|
||||
"product": "Produit",
|
||||
"memory": "Mémoire",
|
||||
@@ -1910,7 +1864,7 @@
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
"description": "Les applications peuvent accéder aux <a href=\"/#/volumes\">volumes</a> montés via le répertoire <code>/media/{volume name}</code>. Ces données ne sont pas incluses dans la sauvegarde de l'application."
|
||||
"description": "Les applications peuvent accéder aux <a href=\"/#/volumes\">volumes</a> montés via le répertoire <code>/media/(volume name)</code>. Ces données ne sont pas incluses dans la sauvegarde de l'application."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
@@ -1943,11 +1897,6 @@
|
||||
"keysEndpoint": "Point de terminaison pour les clés",
|
||||
"tokenEndpoint": "Point de terminaison pour les jetons",
|
||||
"authEndpoint": "Point de terminaison pour l'authentification"
|
||||
},
|
||||
"clients": {
|
||||
"title": "Clients",
|
||||
"newClient": "Nouveau client",
|
||||
"empty": "Aucun client pour le moment"
|
||||
}
|
||||
},
|
||||
"automation": "Automatisation"
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
"openFileManagerActionTooltip": "Apri il File Manager",
|
||||
"name": "Nome",
|
||||
"hostPath": "Percorso Host",
|
||||
"addVolumeAction": "Aggiungi Volume",
|
||||
"title": "Volumi"
|
||||
},
|
||||
"lang": {
|
||||
@@ -169,14 +168,6 @@
|
||||
"customAppUpdateInfo": "Gli aggiornamenti automatici non sono disponibili per le app personalizzate",
|
||||
"updateAvailableAction": "Aggiornamento Disponibile",
|
||||
"title": "Info App"
|
||||
},
|
||||
"auto": {
|
||||
"enableAction": "Abilita aggiornamenti automatici",
|
||||
"disableAction": "Disabilita gli aggiornamenti automatici",
|
||||
"disabled": "Gli aggiornamenti automatici sono attualmente disabilitati.",
|
||||
"enabled": "Gli aggiornamenti automatici sono attualmente abilitati.",
|
||||
"description": "Cloudron controlla periodicamente la presenza di aggiornamenti nell'App Store. Se disabiliti gli aggiornamenti automatici, assicurati di applicare manualmente gli aggiornamenti.",
|
||||
"title": "Aggiornamenti Automatici"
|
||||
}
|
||||
},
|
||||
"storageTabTitle": "Spazio",
|
||||
@@ -241,7 +232,7 @@
|
||||
},
|
||||
"uninstallDialog": {
|
||||
"uninstallAction": "Disinstalla",
|
||||
"description": "Questo disinstallerà immediatamente <b>{{ app }}</b> e rimuoverà tutti i suoi dati.",
|
||||
"description": "Questo disinstallerà immediatamente {{ app }} e rimuoverà tutti i suoi dati.",
|
||||
"title": "Disinstalla {{ app }}"
|
||||
},
|
||||
"appInfo": {
|
||||
@@ -276,8 +267,6 @@
|
||||
"title": "Errore dell'attività"
|
||||
},
|
||||
"recovery": {
|
||||
"disableRecoveryModeAction": "Disabilita modalità ripristino",
|
||||
"enableRecoveryModeAction": "Abilita modalità ripristino",
|
||||
"restartAction": "Riavvia App",
|
||||
"description": "Se l'app non risponde, prova a riavviare l'app. Se l'app si riavvia continuamente a causa di un plug-in danneggiato o di una configurazione errata, posiziona l'app in modalità di ripristino per accedere alla console.\nUtilizza le seguenti <a href=\"{{ docsLink }}\" target=\"_blank\">istruzioni</a> per riavviare l'app.",
|
||||
"title": "Recupero Crash"
|
||||
@@ -303,7 +292,6 @@
|
||||
"cloneTooltip": "Clona da questo backup",
|
||||
"downloadConfigTooltip": "Scarica la configurazione di backup",
|
||||
"time": "Creato alle",
|
||||
"packageVersion": "Versione pacchetto",
|
||||
"description": "I backup sono istantanee complete dell'app. Puoi utilizzare i backup delle app per ripristinare o clonare questa app.",
|
||||
"title": "Backup"
|
||||
}
|
||||
@@ -422,8 +410,7 @@
|
||||
"catchall": {
|
||||
"description": "Le e-mail inviate ad indirizzi non esistenti verranno inoltrati alle caselle seguenti.",
|
||||
"title": "Catch-all",
|
||||
"saveAction": "Salva",
|
||||
"subscriptionRequired": "Questa funzionalità è disponibile solo con un piano a pagamento. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Attiva un piano ora</a>"
|
||||
"saveAction": "Salva"
|
||||
},
|
||||
"mailinglists": {
|
||||
"membersOnlyTooltip": "Pubblicazione limitata ai soli membri",
|
||||
@@ -479,8 +466,7 @@
|
||||
"title": "Firma",
|
||||
"description": "Questo testo verrà inserito in tutte le mail inviate da questo dominio.",
|
||||
"plainTextFormat": "Formato del testo",
|
||||
"saveAction": "Salva",
|
||||
"subscriptionRequired": "Questa funzione è disponibile solo nei piani a pagamento. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\"> Imposta abbonamento adesso </a>"
|
||||
"saveAction": "Salva"
|
||||
},
|
||||
"dnsStatus": {
|
||||
"reSetupAction": "Re-imposta i DNS",
|
||||
@@ -508,10 +494,6 @@
|
||||
"tabTitle": "Stato"
|
||||
},
|
||||
"smtpStatus": {
|
||||
"title": "Stato SMTP",
|
||||
"outboudDirect": "Invio tramite SMTP (diretto)",
|
||||
"blacklistCheck": "Verifica se l'indirizzo IP è in blacklist",
|
||||
"outboudRelay": "Invio tramite SMTP (relay)",
|
||||
"blacklisted": "L'IP di questo server {{ ip }} è su una blacklist.",
|
||||
"notBlacklisted": "L'IP di questo server {{ ip }} <b>non</b> è su una blacklist."
|
||||
},
|
||||
@@ -663,7 +645,7 @@
|
||||
"memoryLimit": "Limite di memoria",
|
||||
"advancedSettings": "Impostazioni avanzate…",
|
||||
"encryptionDescription": "Salva questa passphrase in un luogo sicuro. Cloudron non memorizza questa password. I backup non possono essere decrittografati senza la passphrase",
|
||||
"encryptionPassword": "Password di crittografia (opzionale)",
|
||||
"encryptionPassword": "Password di crittografia",
|
||||
"s3LikeNote": "Rimuovere qualsiasi regola del ciclo di vita di scadenza degli oggetti poiché danneggerà i backup rsync.",
|
||||
"formatChangeNote": "I backup precedenti che utilizzano il vecchio formato di archiviazione devono essere rimossi manualmente.",
|
||||
"format": "Formato Archiviazione",
|
||||
@@ -679,7 +661,6 @@
|
||||
"localDirectory": "Directory di backup locale",
|
||||
"mountPointDescription": "Il punto di montaggio deve essere impostato manualmente. Consulta i <a href=\"{{ providerDocsLink }}\" target=\"_blank\"> documenti </a>.",
|
||||
"mountPoint": "Punto di montaggio",
|
||||
"noopNote": "Questa opzione interrompe la funzionalità di backup e ripristino di Cloudron e deve essere utilizzata solo per i test. Assicurati di aver eseguito il backup completo del server utilizzando metodi alternativi.",
|
||||
"provider": "Provider archiviazione",
|
||||
"title": "Configura archiviazione backup",
|
||||
"uploadPartSizeDescription": "Dimensioni del singolo file nel caricamento multiplo. Fino a 3 parti vengono caricate in parallelo e richiedono maggiore memoria.",
|
||||
@@ -803,12 +784,10 @@
|
||||
"description": "Utilizza questi token di accesso personali per autenticarti all'<a target=\"_blank\" href=\"{{ apiDocsLink }}\"> API Cloudron </a>",
|
||||
"expiresAt": "Scade il",
|
||||
"name": "Nome",
|
||||
"newApiToken": "Nuovo Token API",
|
||||
"title": "Tokens API"
|
||||
},
|
||||
"appPasswords": {
|
||||
"deletePasswordTooltip": "Elimina Password",
|
||||
"newPassword": "Nuova Password",
|
||||
"description": "Le password delle App sono una misura di sicurezza per proteggere il tuo account utente Cloudron. Se devi accedere a un'app Cloudron da un'app mobile o da un client non attendibile, puoi accedere con il tuo nome utente e la password alternativa generata qui.",
|
||||
"noPasswordsPlaceholder": "Nessuna password per l'App creata",
|
||||
"name": "Nome",
|
||||
@@ -954,7 +933,6 @@
|
||||
"externalLdapTooltip": "Dalla directory LDAP esterna",
|
||||
"users": "Utenti",
|
||||
"name": "Nome",
|
||||
"newGroupAction": "Nuovo Gruppo",
|
||||
"title": "Gruppi"
|
||||
},
|
||||
"users": {
|
||||
@@ -972,8 +950,6 @@
|
||||
"user": "Utente",
|
||||
"transferOwnershipTooltip": "Trasferisci Proprietà"
|
||||
},
|
||||
"newUserAction": "Nuovo Utente",
|
||||
"title": "Utenti",
|
||||
"transferOwnershipDialog": {
|
||||
"transferAction": "Trasferisci la proprietà",
|
||||
"description": "L'utente selezionato e l'amministratore di questo Cloudron acquisiranno i permessi di ammministrazione, mentre l'attuale proprietario li perderà.",
|
||||
@@ -1065,7 +1041,6 @@
|
||||
"notAvailableYet": "Non ancora disponibile",
|
||||
"diskContent": "Questo {{ type }} disco contiene"
|
||||
},
|
||||
"title": "Informazioni di sistema",
|
||||
"selectPeriodLabel": "Seleziona il periodo",
|
||||
"cpuUsage": {
|
||||
"graphTitle": "Percentuale",
|
||||
@@ -1099,7 +1074,6 @@
|
||||
"typeBug": "Bug Report",
|
||||
"typeApp": "Errore App",
|
||||
"type": "Tipologia",
|
||||
"subscriptionRequiredDescription": "Puoi trovare risposte nella nostra <a href=\"{{ supportViewLink }}\" target=\"_blank\">documentazione</a> o chiedere nel <a href=\"{{ forumLink }}\" target=\"_blank\">Forum</a>.",
|
||||
"subscriptionRequired": "I ticket di supporto sono disponibili solo nei piani a pagamento.",
|
||||
"title": "Ticket"
|
||||
},
|
||||
@@ -1128,20 +1102,11 @@
|
||||
"disableCheckbox": "Disabilita Aggiornamenti Automatici",
|
||||
"title": "Configura pianificazione aggiornamenti automatici"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"passwordToken": "Password/Token",
|
||||
"email": "Email (Opzionale)",
|
||||
"title": "Configurazione Registro privato"
|
||||
},
|
||||
"privateDockerRegistry": {
|
||||
"configureAction": "Configura Registro",
|
||||
"usernameNotSet": "Non impostato",
|
||||
"username": "Nome utente",
|
||||
"server": "Indirizzo server",
|
||||
"setupSubscriptionAction": "Imposta abbonamento adesso",
|
||||
"subscriptionRequired": "Questa funzione è disponibile solo nei piani a pagamento.",
|
||||
"description": "Cloudron può estrarre e installare <a href=\"{{ customAppsLink }}\" target=\"_blank\">app personalizzate</a> da un registro Docker privato.",
|
||||
"title": "Registro Docker privato"
|
||||
"subscriptionRequired": "Questa funzione è disponibile solo nei piani a pagamento."
|
||||
},
|
||||
"updates": {
|
||||
"stopUpdateAction": "Ferma Aggiornamento",
|
||||
@@ -1159,11 +1124,9 @@
|
||||
"appstoreAccount": {
|
||||
"subscriptionReactivateAction": "Riattiva Abbonamento",
|
||||
"subscriptionChangeAction": "Cambia Abbonamento",
|
||||
"subscriptionSetupAction": "Imposta Abbonamento",
|
||||
"subscriptionEndsAt": "Annullato e termina il",
|
||||
"cloudronId": "ID Cloudron",
|
||||
"subscription": "Abbonamento",
|
||||
"email": "Email Account",
|
||||
"setupAction": "Imposta Account",
|
||||
"description": "Un account Cloudron.io viene utilizzato per accedere all'App Store e gestire l'abbonamento.",
|
||||
"title": "Account Cloudron.io"
|
||||
@@ -1260,8 +1223,6 @@
|
||||
"type": {
|
||||
"spamFilterTrainedInfo": "Filtro antispam addestrato utilizzando il contenuto della casella postale",
|
||||
"deniedInfo": "Connessione da {{ remote.ip }} negata. {{ details.message || details.reason }}",
|
||||
"deliveredInfo": "Posta consegnata a {{ rcptTo | prettyEmailAddresses }} da {{ mailFrom | prettyEmailAddresses }}",
|
||||
"receivedInfo": "Posta salvata da {{ mailFrom | prettyEmailAddresses }} nella casella di posta {{ rcptTo | prettyEmailAddresses }}",
|
||||
"outboundInfo": "Posta in coda per la consegna a {{ rcptTo | prettyEmailAddresses }} da {{ mailFrom | prettyEmailAddresses }}",
|
||||
"inboundInfo": "Posta in arrivo da {{ mailFrom | prettyEmailAddresses }} a {{ rcptTo | prettyEmailAddresses }}. Spam: {{ details.spamStatus.indexOf ('Yes,') === 0 ? 'Si' : 'No' }}",
|
||||
"bounceInfo": "Bounce inviato a {{ mailFrom | prettyEmailAddresses }} per la posta inviata a {{ rcptTo | prettyEmailAddresses }}. {{ details.message || details.reason }}",
|
||||
@@ -1425,7 +1386,6 @@
|
||||
"tooltipEdit": "Modifica Dominio",
|
||||
"provider": "Provider",
|
||||
"domain": "Dominio",
|
||||
"addDomain": "Aggiungi dominio",
|
||||
"title": "Domini e Certificati"
|
||||
},
|
||||
"eventlog": {
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"close": "Đóng",
|
||||
"no": "Không",
|
||||
"yes": "Có",
|
||||
"delete": "Xoá"
|
||||
"delete": "Xoá",
|
||||
"done": "Đã xong",
|
||||
"edit": "Chỉnh sửa"
|
||||
},
|
||||
"username": "Tên đăng nhập",
|
||||
"displayName": "Tên hiển thị",
|
||||
@@ -132,7 +134,7 @@
|
||||
"memoryRequirement": "Cần ít nhất {{ size }} bộ nhớ",
|
||||
"location": "Nơi cài đặt",
|
||||
"locationPlaceholder": "Để trống để dùng tên miền gốc",
|
||||
"manualWarning": "Thêm A record cho <b>{{ nơi cài đặt }}</b> vào địa chỉ IP công cộng của Cloudron này",
|
||||
"manualWarning": "Cài đặt thủ công bản ghi DNS A (IPv4) và AAAA (IPv6) cho <b>{{ location }}</b> chỉ về máy chủ này",
|
||||
"userManagement": "Quản lý người dùng",
|
||||
"userManagementMailbox": "Tất cả người dùng với hộp thư trên Cloudron này có quyền truy cập app.",
|
||||
"userManagementLeaveToApp": "Để app quản lý người dùng",
|
||||
@@ -269,7 +271,6 @@
|
||||
"transferOwnershipTooltip": "Chuyển nhượng quyền sở hữu",
|
||||
"invitationTooltip": "Mời Người dùng",
|
||||
"setGhostTooltip": "Nhập vai",
|
||||
"count": "Tổng ng dùng: {{ count }}",
|
||||
"mailmanagerTooltip": "Người dùng này có thể quản lý những ng dùng khác và cả những hộp thư"
|
||||
},
|
||||
"settings": {
|
||||
@@ -285,11 +286,9 @@
|
||||
"externalLdapTooltip": "Từ thư mục LDAP ngoài",
|
||||
"users": "Người dùng",
|
||||
"name": "Tên",
|
||||
"newGroupAction": "Nhóm mới",
|
||||
"title": "Nhóm"
|
||||
"title": "Nhóm",
|
||||
"emptyPlaceholder": "Chưa có nhóm nào cả"
|
||||
},
|
||||
"newUserAction": "Người dùng mới",
|
||||
"title": "Chỉ mục Người dùng",
|
||||
"editGroupDialog": {
|
||||
"title": "Chỉnh sửa nhóm {{ name }}",
|
||||
"externalLdapWarning": "Nhóm này được đồng bộ từ thư mục LDAP ngoài."
|
||||
@@ -342,8 +341,6 @@
|
||||
},
|
||||
"exposedLdap": {
|
||||
"description": "Cloudron có thể đóng vai trò là máy chủ chỉ mục người dùng trung tâm cho những app bên ngoài.",
|
||||
"enabled": "Đã bật",
|
||||
"title": "Máy chủ chỉ mục",
|
||||
"ipRestriction": {
|
||||
"description": "Giới hạn quyền truy cập máy chủ chỉ mục cho những địa chỉ IP hoặc khoảng vùng cụ thể. Những dòng bắt đầu bằng dấu <code>#</code> được xem như ghi chú thêm.",
|
||||
"placeholder": "Viết xuống dòng những địa chỉ IP hoặc Subnet",
|
||||
@@ -454,7 +451,8 @@
|
||||
"copyNow": "Xin copy mã API này bây giờ. Nó sẽ không được hiển thị lại vì lý do an ninh.",
|
||||
"generateToken": "Tạo mã API",
|
||||
"name": "Tên cho mã API",
|
||||
"access": "Truy cập API"
|
||||
"access": "Truy cập API",
|
||||
"allowedIpRanges": "Dãy IP cho phép"
|
||||
},
|
||||
"enable2FAAction": "Bật xác minh hai bước",
|
||||
"primaryEmail": "Email chính",
|
||||
@@ -463,7 +461,6 @@
|
||||
"app": "App",
|
||||
"name": "Tên",
|
||||
"noPasswordsPlaceholder": "Không có mật khẩu app được tạo",
|
||||
"newPassword": "Mật khẩu mới",
|
||||
"deletePasswordTooltip": "Xoá mật khẩu",
|
||||
"title": "Mật khẩu app",
|
||||
"description": "Mật khẩu app là một biện pháp an ninh giúp bảo vệ tài khoản người dùng Cloudron của bạn. Khi bạn cần truy cập một app trong Cloudron từ một app điện thoại hay client không đáng tin cậy, bạn có thể đăng nhập bằng tên đăng nhập và mật khẩu app thay thế ở đây."
|
||||
@@ -473,14 +470,15 @@
|
||||
"description": "Dùng những mã truy cập cá nhân này để xác minh cho <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a>",
|
||||
"noTokensPlaceholder": "Không có mã API được tạo",
|
||||
"revokeTokenTooltip": "Rút lại mã",
|
||||
"newApiToken": "Mã API mới",
|
||||
"name": "Tên",
|
||||
"expiresAt": "Hết hiệu lực vào",
|
||||
"lastUsed": "Lần dùng cuối",
|
||||
"neverUsed": "chưa từng dùng",
|
||||
"readonly": "Chỉ đọc",
|
||||
"scope": "Mức độ bao phủ",
|
||||
"readwrite": "Đọc và Ghi"
|
||||
"readwrite": "Đọc và Ghi",
|
||||
"allowedIpRangesPlaceholder": "IP hoặc Subnet viết cách ra bởi dấu phẩy",
|
||||
"allowedIpRanges": "IP cho phép"
|
||||
},
|
||||
"loginTokens": {
|
||||
"title": "Mã đăng nhập",
|
||||
@@ -515,12 +513,18 @@
|
||||
"title": "Đã đặt lại mật khẩu thành công",
|
||||
"body": "Email đã được gửi đến {{ email }}"
|
||||
},
|
||||
"enable2FANotAvailable": "Không cài được cho người dùng từ nguồn xác minh ngoài"
|
||||
"enable2FANotAvailable": "Không cài được cho người dùng từ nguồn xác minh ngoài",
|
||||
"removeApiToken": {
|
||||
"title": "Chắc chắn xóa mã token {{ name }}?"
|
||||
},
|
||||
"removeAppPassword": {
|
||||
"title": "Chắc chắn xóa mật khẩu {{ name }}?"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"location": {
|
||||
"title": "Nơi sao lưu",
|
||||
"description": "Cloudron sao lưu toàn bộ hệ thống của bạn vào nơi sao lưu được cài đặt.",
|
||||
"description": "Một bản sao lưu toàn phần của hệ thống được lưu vào vị trí lưu trữ theo định dạng được cấu hình.",
|
||||
"configure": "Cấu hình",
|
||||
"format": "Định dạng lưu trữ",
|
||||
"endpoint": "Điểm Endpoint",
|
||||
@@ -564,7 +568,6 @@
|
||||
"mountPointDescription": "Điểm mount cần được cài đặt thủ công. Xem <a href=\"{{ providerDocsLink }}\" target=\"_blank\">hướng dẫn</a>.",
|
||||
"title": "Cấu hình nơi lưu trữ bản sao lưu",
|
||||
"mountPoint": "Điểm mount",
|
||||
"noopNote": "Lựa chọn này sẽ làm hỏng tính năng sao lưu và khôi phục của Cloudron và chỉ nên dùng khi test hệ thống. Xin đảm bảo rằng server được sao lưu toàn bộ bằng những phương tiện khác.",
|
||||
"format": "Định dạng lưu trữ",
|
||||
"encryptedFilenames": "Tên tập tin đã mã hoá",
|
||||
"chown": "Hệ thống tập tin bên ngoài có hỗ trợ chown",
|
||||
@@ -579,7 +582,8 @@
|
||||
"privateKey": "Mật mã riêng",
|
||||
"diskPath": "Đường dẫn đến ổ đĩa",
|
||||
"cifsSealSupport": "Dùng mã hoá SEAL. Cần SMB thấp nhất là phiên bản v3",
|
||||
"encryptFilenames": "Mã hoá tên tập tin"
|
||||
"encryptFilenames": "Mã hoá tên tập tin",
|
||||
"preserveAttributesLabel": "Giữ nguyên thuộc tính của tập tin"
|
||||
},
|
||||
"cleanupBackups": {
|
||||
"description": "Các bản sao lưu được dọn sạch tự động dựa trên thời gian lưu giữ. Thao tác này sẽ xoá ngay lập tức các bản sao lưu đang có.",
|
||||
@@ -628,7 +632,7 @@
|
||||
"configure": "Cấu hình",
|
||||
"retentionPolicy": "Thời gian lưu giữ",
|
||||
"schedule": "Lịch sao lưu",
|
||||
"description": "Cloudron sao lưu toàn bộ hệ thống của bạn dựa vào định kỳ sao lưu đã lên lịch và giữ các bản sao lưu theo thời gian lưu giữ đã định.",
|
||||
"description": "Một bản sao lưu toàn phần của hệ thống được tạo ra dựa trên Lịch Sao Lưu đặt theo <a href=\"/#/system-locale\">Múi giờ Hệ thống</a>. Các bản sao lưu cũ được xóa bỏ dựa trên Chính Sách Lưu Giữ.",
|
||||
"title": "Lịch sao lưu và thời gian lưu giữ"
|
||||
},
|
||||
"check": {
|
||||
@@ -643,10 +647,27 @@
|
||||
"title": "Chỉnh sửa Bản sao lưu",
|
||||
"label": "Nhãn",
|
||||
"remotePath": "Đường dẫn"
|
||||
},
|
||||
"archives": {
|
||||
"info": "Thông tin",
|
||||
"title": "Kho Lưu Trữ App"
|
||||
},
|
||||
"deleteArchiveDialog": {
|
||||
"description": "Sau khi xóa, bản lưu trữ sẽ được dọn dẹp dựa trên chính sách sao lưu.",
|
||||
"title": "Xóa Lưu trữ của {{appTitle}} ({{fqdn}})"
|
||||
},
|
||||
"restoreArchiveDialog": {
|
||||
"title": "Khôi phục từ Lưu Trữ",
|
||||
"description": "Việc này sẽ cài đặt {{appId}} tại vị trí được xác định từ bản sao lưu hồi {{creationTime}}.",
|
||||
"restoreAction": "Khôi phục",
|
||||
"restoreActionOverwrite": "Khôi phục và tạo DNS mới"
|
||||
},
|
||||
"deleteArchive": {
|
||||
"deleteAction": "Xóa"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"2faToken": "Mã xác minh 2 bước (nếu bật)",
|
||||
"2faToken": "Mã xác minh 2 bước",
|
||||
"resetPasswordAction": "Đặt lại mật khẩu",
|
||||
"signInAction": "Đăng nhập",
|
||||
"password": "Mật khẩu",
|
||||
@@ -728,12 +749,9 @@
|
||||
"incomingServerInfo": "Mail đến (IMAP)",
|
||||
"catchall": {
|
||||
"saveAction": "Lưu",
|
||||
"subscriptionRequired": "Tính năng này chỉ có trong các gói trả phí. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Cài đặt gói trả phí ngay bây giờ</a>",
|
||||
"description": "Những mail gửi đến địa chỉ mail không tồn tại sẽ được chuyển tiếp cho những hộp thư dưới đây.",
|
||||
"title": "Bắt hết cả mail"
|
||||
},
|
||||
"enabled": "Máy chủ mail Cloudron được cài đặt để nhận thư đến cho tên miền này.",
|
||||
"disabled": "Máy chủ mail Cloudron sẽ không nhận thư đến cho tên miền này.",
|
||||
"howToConnectDescription": "Dùng những cài đặt bên dưới để chỉnh client trao đổi mail.",
|
||||
"incomingPasswordUsage": "Mật khẩu của chủ sở hữu hộp thư",
|
||||
"incomingUserInfo": "Tên đăng nhập",
|
||||
@@ -759,7 +777,7 @@
|
||||
},
|
||||
"noopNonAdminDomainWarning": "Cloudron không thể cung cấp dịch vụ gửi mail cho các app trên tên miền này khi chế độ email chưa được bật.",
|
||||
"noopAdminDomainWarning": "Cloudron không thể gửi link mời người dùng, đặt lại mật khẩu hay gửi các thông báo khác khi chế độ email chưa được bật trên tên miền chính",
|
||||
"description": "Cloudron sẽ dùng mail server này (Smart host) để gửi mail ra cho các app cài trên tên miền.",
|
||||
"description": "Mail server này (Smart host) sẽ được dùng để gửi mail ra cho các app được cài đặt trên tên miền này.",
|
||||
"title": "Hệ thống relay chuyển mail ra ngoài"
|
||||
},
|
||||
"mailboxboxDialog": {
|
||||
@@ -814,11 +832,9 @@
|
||||
},
|
||||
"smtpStatus": {
|
||||
"notBlacklisted": "IP của server này {{ ip }} <b>không</b> có trên danh sách chặn.",
|
||||
"blacklistCheck": "Kiểm tra trạng thái IP có bị liệt vào danh sách chặn không",
|
||||
"blacklisted": "IP của server này {{ ip }} đang bị liệt vào danh sách chặn.",
|
||||
"outboudRelay": "SMTP gửi ra (Chuyển tiếp)",
|
||||
"outboudDirect": "SMTP gửi ra (Trực tiếp)",
|
||||
"title": "Trạng thái SMTP"
|
||||
"rblCheck": "Kiểm tra Danh sách đen DNS",
|
||||
"outboundSmtp": "SMTP Gửi thư ra"
|
||||
},
|
||||
"dnsStatus": {
|
||||
"recordNotSet": "chưa được cài",
|
||||
@@ -838,9 +854,8 @@
|
||||
},
|
||||
"signature": {
|
||||
"saveAction": "Lưu",
|
||||
"htmlFormat": "Dạng HTML (không bắt buộc)",
|
||||
"htmlFormat": "Dạng HTML",
|
||||
"plainTextFormat": "Dạng văn bản",
|
||||
"subscriptionRequired": "Tính năng này chỉ có trong các gói trả phí. <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">Cài đặt gói trả phí ngay bây giờ</a>",
|
||||
"description": "Phần chữ này sẽ được gắn thêm vào phía cuối mail gửi đi từ tên miền này.",
|
||||
"title": "Chữ ký cuối mail"
|
||||
},
|
||||
@@ -897,7 +912,7 @@
|
||||
"configure": "Cấu hình",
|
||||
"interface": "Tên giao diện mạng",
|
||||
"provider": "Nhà cung cấp",
|
||||
"description": "Cloudron dùng địa chỉ IPv4 này để cài đặt các bản ghi A của DNS.",
|
||||
"description": "Địa chỉ IPv4 này được dùng để cài đặt các bản ghi A của DNS.",
|
||||
"title": "IPv4",
|
||||
"address": "Địa chỉ IP"
|
||||
},
|
||||
@@ -908,7 +923,7 @@
|
||||
"ipv6": {
|
||||
"address": "Địa chỉ IPv6",
|
||||
"title": "IPv6",
|
||||
"description": "Cloudron dùng địa chỉ IPv6 này để chỉnh các bản ghi DNS AAAA.\n"
|
||||
"description": "Địa chỉ IPv6 này được dùng để cài đặt các bản ghi DNS AAAA."
|
||||
},
|
||||
"configureIpv6": {
|
||||
"title": "Cài đặt nhà cung cấp IPv6"
|
||||
@@ -924,9 +939,9 @@
|
||||
"typeFilterHeader": "Tất cả sự kiện",
|
||||
"solrConfig": {
|
||||
"notEnoughMemory": "Xin dành ra ít nhất 3GB cho mail server để bật Solr.",
|
||||
"enableSolrCheckbox": "Bật tìm kiếm văn bản bằng Solr",
|
||||
"description": "Solr có thể được dùng để tìm kiếm nhanh văn bản trong mail. Solr chỉ có thể chạy khi <a href=\"/#/services\" target=\"_blank\">mail service</a> được chia ít nhất 3GB RAM.",
|
||||
"title": "Tìm kiếm dạng văn bản đầy đủ (Solr)"
|
||||
"enableSolrCheckbox": "Bật tìm kiếm dạng văn bản đầy đủ",
|
||||
"description": "Solr & Tika có thể được dùng để tìm kiếm dạng văn bản đầy đủ nhanh chóng cho email và đính kèm. Solr chỉ có thể hoạt động khi <a href=\"/#/services\" target=\"_blank\">dịch vụ email</a> được chia ít nhất 3GB RAM.",
|
||||
"title": "Tìm kiếm dạng văn bản đầy đủ"
|
||||
},
|
||||
"testMailDialog": {
|
||||
"sendAction": "Gửi",
|
||||
@@ -949,7 +964,7 @@
|
||||
"title": "Thay đổi kích cỡ mail tối đa"
|
||||
},
|
||||
"changeDomainDialog": {
|
||||
"manualInfo": "Thêm A record thủ công cho {{ domain }} vào địa chỉ IP công cộng của Cloudron này",
|
||||
"manualInfo": "Cài đặt thủ công bản ghi A (IPv4) và AAAA (IPv6) cho <b>{{ domain }}</b> chỉ về máy chủ này",
|
||||
"locationPlaceholder": "Để trống để dùng tên miền gốc",
|
||||
"location": "Vị trí",
|
||||
"title": "Thay đổi vị trí đặt mail server",
|
||||
@@ -960,10 +975,8 @@
|
||||
"type": {
|
||||
"spamFilterTrainedInfo": "Bộ lọc spam đã được rèn giũa thêm dựa trên nội dung hộp thư",
|
||||
"deniedInfo": "Kết nối bị từ chối",
|
||||
"deliveredInfo": "Đã gửi thư",
|
||||
"receivedInfo": "Đã lưu thư",
|
||||
"outboundInfo": "Mail đã xếp vào hàng để được gửi đi",
|
||||
"inboundInfo": "Thư đã được nhận",
|
||||
"outboundInfo": "Đang xếp hàng chờ gửi đi",
|
||||
"inboundInfo": "Đang xếp hàng chờ nhận",
|
||||
"deferredInfo": "Không gửi được mail, hệ thống sẽ tự động thử lại sau {{ details.delay }} giây nữa.",
|
||||
"bounceInfo": "Thư bị gửi trả về",
|
||||
"spamFilterTrained": "Bộ lọc spam đã được rèn giũa thêm",
|
||||
@@ -975,7 +988,9 @@
|
||||
"deferred": "Trì hoãn lại",
|
||||
"overQuotaInfo": "Hộp thư {{ mailbox }} đã đầy {{ quotaPercent }}%",
|
||||
"underQuotaInfo": "Hộp thư {{ mailbox }} đã rơi xuống còn {{ quotaPercent }}% của hạn mức",
|
||||
"quota": "Hạn mức hộp thư"
|
||||
"quota": "Hạn mức hộp thư",
|
||||
"savedInfo": "Đã lưu",
|
||||
"sentInfo": "Đã gửi"
|
||||
},
|
||||
"empty": "Log sự kiện hiện đang trống.",
|
||||
"details": "Chi tiết",
|
||||
@@ -986,7 +1001,7 @@
|
||||
"rcptTo": "Gửi cho"
|
||||
},
|
||||
"settings": {
|
||||
"solrFts": "Tìm kiếm dạng văn bản đầy đủ (Solr)",
|
||||
"solrFts": "Tìm kiếm dạng văn bản đầy đủ",
|
||||
"solrNotRunning": "Đang ngừng",
|
||||
"solrRunning": "Đang chạy",
|
||||
"solrEnabled": "Đã bật",
|
||||
@@ -1055,7 +1070,7 @@
|
||||
},
|
||||
"logo": "Logo",
|
||||
"cloudronName": "Tên cho Cloudron",
|
||||
"title": "Thương hiệu",
|
||||
"title": "Giao diện",
|
||||
"backgroundImage": "Hình nền trang đăng nhập",
|
||||
"clearBackgroundImage": "Xoá"
|
||||
},
|
||||
@@ -1071,7 +1086,7 @@
|
||||
"selectPeriodLabel": "Chọn khoảng thời gian",
|
||||
"cpuUsage": {
|
||||
"graphTitle": "Phần trăm sử dụng",
|
||||
"title": "Dung lượng CPU",
|
||||
"title": "CPU",
|
||||
"graphSubtext": "Chỉ những app sử dụng hơn {{ threshold }} cpu mới được hiển thị"
|
||||
},
|
||||
"systemMemory": {
|
||||
@@ -1089,10 +1104,8 @@
|
||||
"uninstalledApp": "App đã xoá",
|
||||
"diskSpeed": "Tốc độ: {{ speed }} MB/s"
|
||||
},
|
||||
"title": "Hệ thống",
|
||||
"info": {
|
||||
"activationTime": "Ngày tạo Cloudron",
|
||||
"platformVersion": "Phiên bản hệ thống",
|
||||
"title": "Thông tin",
|
||||
"vendor": "Nhà cung cấp",
|
||||
"product": "Sản phẩm",
|
||||
@@ -1101,6 +1114,9 @@
|
||||
},
|
||||
"graphs": {
|
||||
"title": "Biểu đồ"
|
||||
},
|
||||
"locale": {
|
||||
"title": "Tùy chỉnh Vùng địa phương"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -1126,7 +1142,6 @@
|
||||
"typeBug": "Báo cáo bug",
|
||||
"typeApp": "Lỗi App",
|
||||
"type": "Loại vấn đề",
|
||||
"subscriptionRequiredDescription": "Bạn có thể tìm thấy câu trả lời trong <a href=\"{{ supportViewLink }}\" target=\"_blank\">hướng dẫn sử dụng</a> hoặc hỏi trên <a href=\"{{ forumLink }}\" target=\"_blank\">Diễn đàn</a>.",
|
||||
"subscriptionRequired": "Phiếu hỗ trợ chỉ có trong những gói trả phí.",
|
||||
"title": "Phiếu hỗ trợ",
|
||||
"emailNotVerified": "Email tài khoản cloudron.io của bạn {{ email }} chưa được xác minh. Xin hãy xác minh mail trước để tạo phiếu hỗ trợ.",
|
||||
@@ -1168,21 +1183,12 @@
|
||||
"title": "Cấu hình lịch cập nhật tự động"
|
||||
},
|
||||
"privateDockerRegistry": {
|
||||
"username": "Tên đăng nhập",
|
||||
"configureAction": "Cấu hình registry",
|
||||
"usernameNotSet": "Chưa được cài đặt",
|
||||
"server": "Địa chỉ server",
|
||||
"setupSubscriptionAction": "Cài đặt gói đăng ký ngay",
|
||||
"subscriptionRequired": "Tính năng này chỉ có trong gói trả phí.",
|
||||
"description": "Cloudron có thể tải hình ảnh về và cài đặt <a href=\"{{ customAppsLink }}\" target=\"_blank\">những app tuỳ chỉnh </a> từ nơi lưu trữ docker registry cá nhân.",
|
||||
"title": "Docker registry cá nhân",
|
||||
"serverNotSet": "Chưa cài đặt"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"passwordToken": "Mật khẩu/Mật mã",
|
||||
"email": "Email (Không bắt buộc)",
|
||||
"title": "Cấu hình registry cá nhân"
|
||||
},
|
||||
"updates": {
|
||||
"checkForUpdatesAction": "Kiểm tra cập nhật",
|
||||
"stopUpdateAction": "Dừng cập nhật",
|
||||
@@ -1190,26 +1196,27 @@
|
||||
"changeScheduleAction": "Thay đổi lịch cập nhật",
|
||||
"showLogsAction": "Hiển thị log",
|
||||
"version": "Phiên bản hệ thống",
|
||||
"title": "Cập nhật"
|
||||
"title": "Cập nhật",
|
||||
"disabled": "Đã tắt",
|
||||
"schedule": "Lịch cập nhật",
|
||||
"description": "Cập nhật Hệ thống và Ứng dụng được thực hiện tự động dựa trên Lịch cập nhật trong <a href=\"/#/settings\">Múi giờ hệ thống</a>."
|
||||
},
|
||||
"timezone": {
|
||||
"description": "Múi giờ hiện tại là ở <b>{{ timeZone }}</b>. Cài đặt này được dùng cho tác vụ sao lưu và cập nhật. Dấu thời gian hiện ở giao diện được hiển thị theo múi giờ của trình duyệt hiện dùng.",
|
||||
"title": "Múi giờ"
|
||||
"title": "Múi giờ hệ thống"
|
||||
},
|
||||
"appstoreAccount": {
|
||||
"subscriptionReactivateAction": "Kích hoạt lại gói đăng ký",
|
||||
"subscriptionChangeAction": "Quản lý gói đăng ký",
|
||||
"subscriptionSetupAction": "Nâng cấp Gói Cao cấp",
|
||||
"subscriptionEndsAt": "Đã huỷ đăng ký và kết thúc vào",
|
||||
"cloudronId": "Mã Cloudron ID",
|
||||
"subscription": "Gói đăng ký",
|
||||
"email": "Email tài khoản",
|
||||
"setupAction": "Cài đặt tài khoản",
|
||||
"description": "Tài khoản Cloudron.io được dùng để truy cập Cửa hàng App và quản lý gói đăng ký.",
|
||||
"title": "Tài khoản Cloudron.io",
|
||||
"emailNotVerified": "Địa chỉ email chưa được xác minh"
|
||||
},
|
||||
"title": "Cài đặt"
|
||||
"title": "Hệ thống"
|
||||
},
|
||||
"services": {
|
||||
"configure": {
|
||||
@@ -1227,7 +1234,7 @@
|
||||
"memoryLimit": "Giới hạn bộ nhớ",
|
||||
"memoryUsage": "Dung lượng bộ nhớ sử dụng",
|
||||
"service": "Dịch vụ",
|
||||
"description": "Những dịch vụ trong Cloudron mang đến các tính năng như cơ sở dữ liệu, email và xác thực.",
|
||||
"description": "Các dịch vụ dùng cho những tính năng như cơ sở dữ liệu, email và xác thực.",
|
||||
"title": "Dịch vụ",
|
||||
"refresh": "Làm mới (refresh)"
|
||||
},
|
||||
@@ -1371,7 +1378,22 @@
|
||||
"dismissTooltip": "Xoá",
|
||||
"nonePending": "Đã xem tất cả!",
|
||||
"title": "Thông báo",
|
||||
"markAllAsRead": "Đánh dấu đã xem qua tất cả"
|
||||
"markAllAsRead": "Đánh dấu đã xem qua tất cả",
|
||||
"settings": {
|
||||
"appOutOfMemory": "App đã chạy hết bộ nhớ",
|
||||
"appDown": "App bị sụp nguồn",
|
||||
"title": "Cài đặt thông báo",
|
||||
"backupFailed": "Sao lưu thất bại",
|
||||
"certificateRenewalFailed": "Gia hạn chứng chỉ thất bại",
|
||||
"appUp": "App đã hoạt động trở lại bình thường",
|
||||
"rebootRequired": "Cần khởi động lại máy chủ",
|
||||
"cloudronUpdateFailed": "Cập nhật Cloudron thất bại",
|
||||
"diskSpace": "Sắp hết dung lượng ổ đĩa"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "Một email sẽ được gửi để thông báo những sự kiện được chọn đến địa chỉ email chính của bạn."
|
||||
},
|
||||
"allCaughtUp": "Đã xem tất cả"
|
||||
},
|
||||
"domains": {
|
||||
"removeDialog": {
|
||||
@@ -1395,7 +1417,7 @@
|
||||
"zoneName": "Tên zone (không bắt buộc)",
|
||||
"advancedAction": "Cài đặt nâng cao…",
|
||||
"letsEncryptInfo": "Let’s Encrypt cần cổng 80 trên server này mở để kết nối",
|
||||
"wildcardInfo": "Cài đặt bản ghi <i>A</i> cho <b>*.{{ domain }}</b> và <b>{{ domain }}</b> vào địa chỉ IP của server này.",
|
||||
"wildcardInfo": "Cài đặt thủ công bản ghi DNS A (IPv4) và AAAA (IPv6) cho <b>*.{{ domain }}.</b> và <b>{{ domain }}.</b> chỉ về máy chủ này",
|
||||
"manualInfo": "Tất cả các bản ghi DNS cần được cài đặt thủ công trước khi cài đặt mỗi app.",
|
||||
"namecheapInfo": "IP của server này còn được cho phép sử dụng mã API này.",
|
||||
"namecheapApiKey": "Mã API",
|
||||
@@ -1432,7 +1454,13 @@
|
||||
"ovhAppSecret": "Mã bí mật App",
|
||||
"ovhEndpoint": "Điểm Endpoint",
|
||||
"ovhConsumerKey": "Mã Khách hàng",
|
||||
"ovhAppKey": "Mã App"
|
||||
"ovhAppKey": "Mã App",
|
||||
"gandiTokenType": "Loại mã",
|
||||
"gandiTokenTypeApiKey": "Mã API (Đã loại bỏ)",
|
||||
"gandiTokenTypePAT": "Mã truy cập cá nhân (PAT)",
|
||||
"inwxUsername": "Tên đăng nhập",
|
||||
"inwxPassword": "Mật khẩu",
|
||||
"customNameservers": "Tên miền sử dụng nameserver riêng"
|
||||
},
|
||||
"subscriptionRequired": {
|
||||
"description": "Để thêm tên miền, hãy đăng ký gói trả phí.",
|
||||
@@ -1449,7 +1477,7 @@
|
||||
"renewCerts": {
|
||||
"showLogsAction": "Hiển thị log",
|
||||
"renewAllAction": "Gia hạn tất cả CCS",
|
||||
"description": "Cloudron gia hạn tự động chứng chỉ số của Let’s Encrypt. Sử dụng lựa chọn này để kích hoạt lệnh gia hạn ngay lập tức.",
|
||||
"description": "Chứng chỉ số Let’s Encrypt được gia hạn tự động. Dùng lựa chọn này để kích hoạt lệnh gia hạn ngay lập tức.",
|
||||
"title": "Gia hạn chứng chỉ số"
|
||||
},
|
||||
"title": "Tên miền & CCS",
|
||||
@@ -1457,7 +1485,6 @@
|
||||
"tooltipEdit": "Chỉnh tên miền",
|
||||
"provider": "Nhà cung cấp",
|
||||
"domain": "Tên miền",
|
||||
"addDomain": "Thêm tên miền",
|
||||
"syncDns": {
|
||||
"title": "Đồng bộ DNS",
|
||||
"description": "Lựa chọn này sẽ cấp lại các bản ghi DNS cho app và email cho tất cả tên miền.",
|
||||
@@ -1481,17 +1508,19 @@
|
||||
"openAction": "Mở {{ app }}",
|
||||
"postInstallConfirmCheckbox": "Đã xem hướng dẫn",
|
||||
"appDocsUrl": "Xin xem phần <a target=\"_blank\" href=\"{{ docsUrl }}\">{{ title }} hướng dẫn</a> để xem những thông tin hữu ích và chủ đề thường gặp của app này. Nếu bạn cần hỗ trợ thêm, hãy ghé xem trong<a target=\"_blank\" href=\"{{ forumUrl }}\"> diễn đàn {{ title }}</a>.",
|
||||
"checklist": "Danh sách kiểm tra cho Admin"
|
||||
"checklist": "Danh sách kiểm tra cho Admin",
|
||||
"checklistHide": "Ẩn Danh sách việc cần làm",
|
||||
"checklistShow": "Xem Danh sách việc cần làm"
|
||||
},
|
||||
"uninstall": {
|
||||
"uninstall": {
|
||||
"uninstallAction": "Xoá",
|
||||
"description": "Việc này sẽ xóa app ngay lập tức và tất cả dữ liệu. Trang sẽ không còn truy cập được sau khi xóa.",
|
||||
"description": "Việc này sẽ gỡ cài đặt app và xóa tất cả dữ liệu trong app. Các bản sao lưu sẽ được dọn dẹp dựa trên chính sách sao lưu.",
|
||||
"title": "Xoá"
|
||||
},
|
||||
"startStop": {
|
||||
"stopAction": "Dừng app",
|
||||
"startAction": "Chạy app",
|
||||
"stopAction": "Dừng",
|
||||
"startAction": "Khởi động",
|
||||
"description": "App có thể được dừng chạy để bảo tồn tài nguyên server thay vì xoá app. Những bản sao lưu tương lai sẽ không bao gồm những thay đổi từ thời điểm này đến bản sao lưu kề cận nhất. Vì lý do này, bạn nên tạo một bản sao lưu trước khi cho dừng app.",
|
||||
"title": "Chạy / Dừng"
|
||||
}
|
||||
@@ -1500,10 +1529,8 @@
|
||||
"appIsBusyTooltip": "App đang bận",
|
||||
"recovery": {
|
||||
"description": "Nếu app không có phản hồi, hãy thử khởi động lại app. Nếu app bị tự khởi động lại liên tục vì một plugin hay cấu hình hỏng, hãy bật app vào chế độ phục hồi để truy cập vào màn hình console. \nDùng những <a href=\"{{ docsLink }}\" target=\"_blank\">hướng dẫn sau đây</a> để khởi chạy app trở lại.",
|
||||
"disableRecoveryModeAction": "Tắt chế độ phục hồi",
|
||||
"enableRecoveryModeAction": "Bật chế độ phục hồi",
|
||||
"title": "Khôi phục khi app bị sụp",
|
||||
"restartAction": "Khởi động lại app"
|
||||
"restartAction": "Khởi động lại"
|
||||
},
|
||||
"taskError": {
|
||||
"retryAction": "Thử lại tác vụ {{ task }}",
|
||||
@@ -1517,7 +1544,7 @@
|
||||
"disableAction": "Tắt chế độ sao lưu tự động",
|
||||
"disabled": "Sao lưu tự động hiện đang tắt.",
|
||||
"enabled": "Sao lưu tự động đang được bật.",
|
||||
"description": "Cloudron định kỳ tạo ra bản sao lưu dựa trên cài đặt trong phần <a href=\"{{ backupLink }}\">sao lưu</a> .",
|
||||
"description": "Các bản sao lưu được tự động tạo ra dựa trên <a href=\"{{ backupLink }}\">Lịch sao lưu</a> .",
|
||||
"title": "Bản sao lưu tự động"
|
||||
},
|
||||
"import": {
|
||||
@@ -1531,21 +1558,12 @@
|
||||
"cloneTooltip": "Nhân bản app từ bản sao lưu này",
|
||||
"downloadConfigTooltip": "Tải xuống cấu hình bản sao lưu",
|
||||
"time": "Tạo ra lúc",
|
||||
"packageVersion": "Phiên bản đóng gói",
|
||||
"description": "Bản sao lưu là những bản chụp snapshot hoàn chỉnh của app. Bạn có thể dùng các bản sao lưu để khôi phục hoặc nhân bản app này.",
|
||||
"title": "Bản sao lưu",
|
||||
"downloadBackupTooltip": "Tải bản sao lưu"
|
||||
}
|
||||
},
|
||||
"updates": {
|
||||
"auto": {
|
||||
"enableAction": "Bật cập nhật tự động",
|
||||
"disableAction": "Tắt cập nhật tự động",
|
||||
"disabled": "Cập nhật tự động hiện đang tắt.",
|
||||
"enabled": "Cập nhật tự dộng đang được mở.",
|
||||
"description": "Cloudron định kỳ kiểm tra <a href=\"{{ appStoreLink }}\" target=\"_blank\">Cửa hàng App </a> cho phiên bản app mới.",
|
||||
"title": "Cập nhật tự động"
|
||||
},
|
||||
"info": {
|
||||
"updateAvailableAction": "Có phiên bản cập nhật mới",
|
||||
"customAppUpdateInfo": "Tự động cập nhật không có sẵn cho các app tùy chỉnh.",
|
||||
@@ -1579,7 +1597,7 @@
|
||||
},
|
||||
"from": {
|
||||
"saveAction": "Lưu",
|
||||
"mailboxPlaceholder": "Để trống để dùng giá trị mặc định của hệ thống",
|
||||
"mailboxPlaceholder": "Tên Hộp thư",
|
||||
"description": "Lựa chọn này cài đặt tên địa chỉ mà app sẽ gửi mail ra. App này đã được cài đặt để gửi mail trong phần cài đặt {{ domain }}'s <a href=\"\\{{ domainConfigLink }}\\\">Mail gửi ra</a>.",
|
||||
"title": "Địa chỉ mail GỬI TỪ (FROM)",
|
||||
"enable": "Dùng Mail Cloudron để gửi mail",
|
||||
@@ -1604,7 +1622,9 @@
|
||||
"7d": "7 ngày trước",
|
||||
"24h": "24 tiếng trước",
|
||||
"12h": "12 tiếng trước",
|
||||
"6h": "6 tiếng"
|
||||
"6h": "6 tiếng",
|
||||
"live": "Trực tiếp",
|
||||
"1h": "1 tiếng"
|
||||
},
|
||||
"diskTitle": "Dung lượng ổ đĩa",
|
||||
"diskIOTotal": "tổng: đọc {{ read }} / ghi {{ write }}",
|
||||
@@ -1616,7 +1636,7 @@
|
||||
"addMountAction": "Gắn thêm một volume vào",
|
||||
"noMounts": "Không có volume được gắn thêm.",
|
||||
"volume": "Volume",
|
||||
"title": "Thư mục mount thêm",
|
||||
"title": "Volume gắn thêm",
|
||||
"readOnly": "Chỉ cho phép đọc",
|
||||
"permissions": {
|
||||
"readOnly": "Chỉ cho phép đọc",
|
||||
@@ -1678,7 +1698,8 @@
|
||||
"redirectionsPlaceholder": "Để trống để dùng tên miền gốc",
|
||||
"redirections": "Chuyển hướng",
|
||||
"locationPlaceholder": "Để trống để dùng tên miền gốc",
|
||||
"location": "Nơi cài đặt"
|
||||
"location": "Nơi cài đặt",
|
||||
"dnsoverwrite": "Một vài bản ghi DNS đã có sẵn. Đồng ý ghi đè lên."
|
||||
},
|
||||
"display": {
|
||||
"saveAction": "Lưu",
|
||||
@@ -1732,7 +1753,9 @@
|
||||
"restoreAction": "Khôi phục",
|
||||
"warning": "Những dữ liệu được tạo ra tính từ thời điểm này và lần sao lưu cuối sẽ bị mất vĩnh viễn. Bạn nên tạo một bản sao lưu của những dữ liệu hiện tại trước khi thực hiện việc khôi phục app.",
|
||||
"description": "Lựa chọn này sẽ khôi phục app về phần dữ liệu từ {{ creationTime }}.",
|
||||
"title": "Khôi phục {{ app }}"
|
||||
"title": "Khôi phục {{ app }}",
|
||||
"cloneAction": "Nhân bản",
|
||||
"cloneActionOverwrite": "Nhân bản và ghi đè lên DNS"
|
||||
},
|
||||
"updateDialog": {
|
||||
"updateAction": "Cập nhật",
|
||||
@@ -1761,7 +1784,7 @@
|
||||
},
|
||||
"uninstallDialog": {
|
||||
"uninstallAction": "Xoá",
|
||||
"description": "Lựa chọn này sẽ ngay lập tức xoá app <b>{{ app }}</b> và xoá toàn bộ dữ liệu trong đó.",
|
||||
"description": "Lựa chọn này sẽ xoá {{ app }} và xoá toàn bộ dữ liệu trong đó.",
|
||||
"title": "Xoá {{ app }}"
|
||||
},
|
||||
"stopDialog": {
|
||||
@@ -1792,12 +1815,14 @@
|
||||
"turn": {
|
||||
"title": "Cài đặt TURN",
|
||||
"enable": "Thiết lập app để sử dụng máy chủ TURN được cài sẵn",
|
||||
"disable": "Không thiết lập TURN cho app này. Các cài đặt TURN cho app được giữ nguyên. Bạn có thể tuỳ chỉnh thêm trong app."
|
||||
"disable": "Không thiết lập TURN cho app này. Các cài đặt TURN cho app được giữ nguyên. Bạn có thể tuỳ chỉnh thêm trong app.",
|
||||
"info": "Bật chế độ này để app sử dụng máy chủ TURN được cài sẵn. Khi tắt, cài đặt TURN của app sẽ được để yên."
|
||||
},
|
||||
"redis": {
|
||||
"title": "Thiết lập Redis",
|
||||
"enable": "Thiết lập app sử dụng Redis",
|
||||
"disable": "Tắt Redis"
|
||||
"disable": "Tắt Redis",
|
||||
"info": "Nếu bật, app sẽ sử dụng dịch vụ Redis có sẵn. Khi tắt, cài đặt Redis của app sẽ được giữ nguyên."
|
||||
},
|
||||
"addApplinkDialog": {
|
||||
"title": "Thêm link app bên ngoài"
|
||||
@@ -1817,7 +1842,20 @@
|
||||
"notes": {
|
||||
"title": "Ghi chú của Admin"
|
||||
}
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"title": "Kho lưu trữ",
|
||||
"description": "Bản cập nhật mới nhất sẽ được thêm vào <a href=\"/#/backups\">Kho lưu trữ</a>. App sẽ được gỡ bỏ sau đó, nhưng vẫn có thể được khôi phục từ Mục Sao lưu. Những bản sao lưu trước đó sẽ được dọn dẹp dựa trên chính sách sao lưu.",
|
||||
"action": "Kho lưu trữ",
|
||||
"latestBackupInfo": "Bản sao lưu mới nhất được tạo lúc {{date}}.",
|
||||
"noBackup": "App chưa có bản sao lưu. Cần một bản sao lưu gần nhất để lưu trữ."
|
||||
},
|
||||
"archiveDialog": {
|
||||
"title": "Lưu trữ {{app}}",
|
||||
"description": "Việc này sẽ xóa app và đặt bản sao lưu app mới nhất được tạo ra lúc {{date}} trong Kho lưu trữ App."
|
||||
},
|
||||
"updateAvailableTooltip": "Có phiên bản cập nhật mới",
|
||||
"configureTooltip": "Cấu hình"
|
||||
},
|
||||
"volumes": {
|
||||
"name": "Tên volume",
|
||||
@@ -1845,7 +1883,6 @@
|
||||
"removeVolumeActionTooltip": "Xoá volume",
|
||||
"openFileManagerActionTooltip": "Mở Quản lý tập tin",
|
||||
"hostPath": "Điểm đến",
|
||||
"addVolumeAction": "Thêm volume",
|
||||
"updateVolumeDialog": {
|
||||
"title": "Cập nhật Volume {{ volume }}"
|
||||
},
|
||||
@@ -1928,17 +1965,17 @@
|
||||
},
|
||||
"storage": {
|
||||
"mounts": {
|
||||
"description": "Các app có thể truy cập vào <a href=\"/#/volumes\">những volume</a> được mount lên thông qua thư mục <code>/media/{volume name}</code>. Dữ liệu này không được bao gồm trong phần bản sao lưu của app."
|
||||
"description": "Các app có thể truy cập vào <a href=\"/#/volumes\">những volume</a> được mount lên thông qua thư mục <code>/media/(volume name)</code>. Dữ liệu này không được bao gồm trong phần bản sao lưu của app."
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"newClientDialog": {
|
||||
"title": "Thêm client",
|
||||
"description": "Thêm cài đặt client kết nối OpenID mới.",
|
||||
"createAction": "Tạo"
|
||||
"title": "Thêm OIDC client",
|
||||
"description": "Nhập cài đặt OIDC client mới",
|
||||
"createAction": "Thêm"
|
||||
},
|
||||
"client": {
|
||||
"loginRedirectUri": "Đường dẫn callback khi đăng nhập (viết cách ra bởi dấu phẩy nếu có nhiều hơn một)",
|
||||
"loginRedirectUri": "Đường dẫn callback khi đăng nhập (viết cách ra bởi dấu phẩy)",
|
||||
"name": "Tên",
|
||||
"id": "ID client",
|
||||
"secret": "Mật khẩu client",
|
||||
@@ -1946,18 +1983,13 @@
|
||||
"logoutRedirectUri": "Đường dẫn callback khi đăng nhập (không bắt buộc)"
|
||||
},
|
||||
"description": "Cloudron có thể làm nhà cung cấp kết nối OpenID cho các app trong và ngoài hệ thống.",
|
||||
"clients": {
|
||||
"title": "Client",
|
||||
"newClient": "Thêm client mới",
|
||||
"empty": "Chưa có client"
|
||||
},
|
||||
"title": "Nhà cung cấp kết nối OpenID",
|
||||
"title": "Nhà cung cấp OpenID",
|
||||
"editClientDialog": {
|
||||
"title": "Chỉnh sửa client {{ client }}"
|
||||
},
|
||||
"deleteClientDialog": {
|
||||
"title": "Chắc chắn muốn xoá client {{ client }}?",
|
||||
"description": "Thao tác này sẽ ngắt kết nối tất cả app OpenID bên ngoài có trong Cloudron sử dụng ID client này."
|
||||
"description": "Xóa OIDC client này sẽ vô hiệu hóa tất cả mã truy cập. Những app sử dụng OIDC client này sẽ không còn xác minh được theo cách này nữa."
|
||||
},
|
||||
"env": {
|
||||
"discoveryUrl": "Đường dẫn Tìm kiếm",
|
||||
@@ -1968,5 +2000,10 @@
|
||||
"tokenEndpoint": "Điểm cuối token"
|
||||
}
|
||||
},
|
||||
"automation": "Tự động hoá"
|
||||
"automation": "Tự động hoá",
|
||||
"userdirectory": {
|
||||
"settings": {
|
||||
"title": "Cài đặt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,12 +103,10 @@
|
||||
"title": "App 密码",
|
||||
"app": "App",
|
||||
"description": "App 密码是一种用来保护您的 Cloudron 账号的安全措施,如果您需要从不信任的移动应用或者客户端登录 Cloudron 应用,请使用你的用户名和此处生成的代用密码。",
|
||||
"newPassword": "新密码",
|
||||
"deletePasswordTooltip": "删除密码"
|
||||
},
|
||||
"apiTokens": {
|
||||
"title": "API Tokens",
|
||||
"newApiToken": "新 API Token",
|
||||
"name": "名称",
|
||||
"expiresAt": "过期时间",
|
||||
"description": "使用这些个人 access tokens 来进行 <a target=\"_blank\" href=\"{{ apiDocsLink }}\">Cloudron API</a> 认证",
|
||||
@@ -208,7 +206,6 @@
|
||||
"s3SecretAccessKey": "Secret access key",
|
||||
"gcsServiceKey": "Service Account Key",
|
||||
"format": "存储格式",
|
||||
"noopNote": "这个选项会停用 Cloudron 的备份和恢复功能,仅应该被用于测试。请确保这台服务器已经使用其它方式备份。",
|
||||
"formatChangeNote": "使用旧存储格式的备份需要被手动删除。",
|
||||
"encryptionPassword": "加密密码(可选)",
|
||||
"advancedSettings": "高级设置…",
|
||||
@@ -389,8 +386,6 @@
|
||||
"searchPlaceholder": "使用应用名称如 Github, Dropbox, Slack, Trello, ... 来搜索替代品"
|
||||
},
|
||||
"users": {
|
||||
"title": "用户目录",
|
||||
"newUserAction": "新用户",
|
||||
"users": {
|
||||
"user": "用户",
|
||||
"groups": "用户组",
|
||||
@@ -407,12 +402,10 @@
|
||||
"transferOwnershipTooltip": "转让所有权",
|
||||
"invitationTooltip": "邀请用户",
|
||||
"setGhostTooltip": "模拟该用户",
|
||||
"mailmanagerTooltip": "该用户可以管理用户和邮箱",
|
||||
"count": "用户总数: {{ count }}"
|
||||
"mailmanagerTooltip": "该用户可以管理用户和邮箱"
|
||||
},
|
||||
"groups": {
|
||||
"title": "用户组",
|
||||
"newGroupAction": "新用户组",
|
||||
"name": "名称",
|
||||
"users": "用户",
|
||||
"externalLdapTooltip": "使用外部 LDAP 目录"
|
||||
@@ -552,9 +545,7 @@
|
||||
"label": "密钥",
|
||||
"description": "所有 LDAP 请求都必须使用这个密钥和用户 DN <i>{{ userDN }}</i> 认证身份"
|
||||
},
|
||||
"title": "目录服务器",
|
||||
"description": "Cloudron 可以作为用户目录给其他外部应用使用。",
|
||||
"enabled": "启用",
|
||||
"ipRestriction": {
|
||||
"description": "该目录服务可以仅允许特定 IP 或 IP 段使用。",
|
||||
"placeholder": "每行一个 IP 地址或子网",
|
||||
@@ -697,8 +688,6 @@
|
||||
"bounce": "退信",
|
||||
"deferredInfo": "发送邮件失败,会在 {{ details.delay }} 秒后重试。",
|
||||
"outboundInfo": "已加入送信队列",
|
||||
"receivedInfo": "已保存",
|
||||
"deliveredInfo": "邮件已送达",
|
||||
"spamFilterTrainedInfo": "垃圾邮件过滤器使用邮箱的内容进行训练",
|
||||
"spamFilterTrained": "垃圾邮件过滤器已训练",
|
||||
"bounceInfo": "发送退信通知",
|
||||
@@ -784,11 +773,9 @@
|
||||
"title": "Cloudron.io 账户",
|
||||
"description": "Cloudron.io 账户用来使用 App Store 和管理订阅。",
|
||||
"setupAction": "设置账户",
|
||||
"email": "账户 Email",
|
||||
"subscription": "订阅",
|
||||
"cloudronId": "Cloudron ID",
|
||||
"subscriptionEndsAt": "已取消并将终止于",
|
||||
"subscriptionSetupAction": "设置订阅",
|
||||
"subscriptionChangeAction": "更改订阅",
|
||||
"subscriptionReactivateAction": "重新激活订阅"
|
||||
},
|
||||
@@ -806,21 +793,12 @@
|
||||
"stopUpdateAction": "停止更新"
|
||||
},
|
||||
"privateDockerRegistry": {
|
||||
"title": "私有 Docker 仓库",
|
||||
"subscriptionRequired": "这项功能仅在付费计划中可用。",
|
||||
"server": "服务器地址",
|
||||
"username": "用户名",
|
||||
"usernameNotSet": "未设置",
|
||||
"configureAction": "配置仓库",
|
||||
"description": "Cloudron 可以安装从私有 Docker 仓库安装 <a href=\"{{ customAppsLink }}\" target=\"_blank\">自定义的应用</a>。",
|
||||
"setupSubscriptionAction": "现在设置订阅",
|
||||
"serverNotSet": "未设置"
|
||||
},
|
||||
"privateDockerRegistryDialog": {
|
||||
"title": "私有仓库设置",
|
||||
"email": "邮件(可选)",
|
||||
"passwordToken": "密码 / Token"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"title": "配置自动更新时间表",
|
||||
"disableCheckbox": "停用自动更新",
|
||||
@@ -854,7 +832,6 @@
|
||||
"ticket": {
|
||||
"title": "工单",
|
||||
"subscriptionRequired": "支持工单仅在付费订阅中可用。",
|
||||
"subscriptionRequiredDescription": "您或许能在我们的<a href=\"{{ supportViewLink }}\" target=\"_blank\">文档</a>中找到答案,或者在<a href=\"{{ forumLink }}\" target=\"_blank\">论坛</a>上提问。",
|
||||
"type": "类型",
|
||||
"typeApp": "应用错误",
|
||||
"typeBug": "Bug 反馈",
|
||||
@@ -879,7 +856,6 @@
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "系统信息",
|
||||
"diskUsage": {
|
||||
"title": "硬盘使用",
|
||||
"usageInfo": "<b>{{ size | prettyDiskSize }}</b> 中的 {{ available | prettyDiskSize }}</b> 可用",
|
||||
@@ -907,7 +883,6 @@
|
||||
},
|
||||
"domains": {
|
||||
"title": "域名和证书",
|
||||
"addDomain": "添加域名",
|
||||
"domain": "域名",
|
||||
"provider": "提供商",
|
||||
"tooltipEdit": "编辑域名",
|
||||
@@ -1141,7 +1116,6 @@
|
||||
"incomingServerInfo": "入站邮件(IMAP)",
|
||||
"catchall": {
|
||||
"saveAction": "保存",
|
||||
"subscriptionRequired": "这个功能仅付费订阅可用。 <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">现在设置订阅</a>",
|
||||
"description": "若收件地址不存在,邮件会被转发给下列邮箱。",
|
||||
"title": "邮件转移"
|
||||
}
|
||||
@@ -1162,11 +1136,7 @@
|
||||
},
|
||||
"smtpStatus": {
|
||||
"notBlacklisted": "当前服务器 IP {{ ip }} <b>不在</b> 黑名单上。",
|
||||
"blacklisted": "当前服务器 IP {{ ip }} 在黑名单上。",
|
||||
"blacklistCheck": "IP 地址黑名单检查",
|
||||
"outboudDirect": "出站 SMTP(直连)",
|
||||
"outboudRelay": "出站 SMTP(中继)",
|
||||
"title": "SMTP 状态"
|
||||
"blacklisted": "当前服务器 IP {{ ip }} 在黑名单上。"
|
||||
},
|
||||
"dnsStatus": {
|
||||
"recordNotSet": "未设置",
|
||||
@@ -1188,7 +1158,6 @@
|
||||
"saveAction": "保存",
|
||||
"htmlFormat": "HTML 格式(可选)",
|
||||
"plainTextFormat": "纯文本格式",
|
||||
"subscriptionRequired": "本功能仅在付费订阅中可用。 <a href=\"\" class=\"pull-right\" ng-click=\"openSubscriptionSetup()\">现在设置付费订阅</a>",
|
||||
"description": "下列文本会被附在所有从本域名发出的邮件的末尾。",
|
||||
"title": "签名"
|
||||
},
|
||||
@@ -1418,14 +1387,6 @@
|
||||
"packageVersion": "打包版本",
|
||||
"lastUpdated": "最后更新",
|
||||
"checkForUpdatesAction": "检查更新"
|
||||
},
|
||||
"auto": {
|
||||
"title": "自动更新",
|
||||
"disableAction": "停用自动更新",
|
||||
"disabled": "自动更新已停用。",
|
||||
"description": "Cloudron 会定时从 App Store 检查更新。如果要停用自动更新,请确保你会手动更新应用。",
|
||||
"enabled": "自动更新已开启。",
|
||||
"enableAction": "启用自动更新"
|
||||
}
|
||||
},
|
||||
"repair": {
|
||||
@@ -1435,9 +1396,7 @@
|
||||
"retryAction": "重试 {{ task }} 任务"
|
||||
},
|
||||
"recovery": {
|
||||
"enableRecoveryModeAction": "启用恢复模式",
|
||||
"description": "如果应用无法响应,请先尝试重启应用。如果应用由于插件或者配置错误,在重启后仍然无法响应,请使用恢复模式来启动。\n请尝试下列<a href=\"{{ docsLink }}\" target=\"_blank\">步骤</a>来恢复该应用。",
|
||||
"disableRecoveryModeAction": "停用恢复模式",
|
||||
"title": "崩溃恢复",
|
||||
"restartAction": "重启应用"
|
||||
},
|
||||
@@ -1457,7 +1416,7 @@
|
||||
"uninstallDialog": {
|
||||
"uninstallAction": "删除",
|
||||
"title": "卸载 {{ app }}",
|
||||
"description": "此操作会立即卸载 <b>{{ app }}</b> 并删除它的所有数据。"
|
||||
"description": "此操作会立即卸载 {{ app }} 并删除它的所有数据。"
|
||||
},
|
||||
"updateDialog": {
|
||||
"changelogHeader": "版本 {{ version}} 的更新:",
|
||||
@@ -1475,7 +1434,6 @@
|
||||
"importAction": "导入备份",
|
||||
"title": "备份",
|
||||
"description": "备份是应用的完整快照。你可以使用应用的备份来恢复或者克隆该应用。",
|
||||
"packageVersion": "打包版本",
|
||||
"time": "创建于",
|
||||
"downloadConfigTooltip": "下载备份的配置文件",
|
||||
"cloneTooltip": "由此备份克隆"
|
||||
@@ -1606,7 +1564,6 @@
|
||||
"addAction": "添加",
|
||||
"title": "添加磁盘卷"
|
||||
},
|
||||
"addVolumeAction": "添加卷",
|
||||
"name": "名称",
|
||||
"hostPath": "主机路径",
|
||||
"removeVolumeDialog": {
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
<!-- Modal postinstall confirm -->
|
||||
<div class="modal fade" id="appsPostInstallConfirmModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appPostInstallConfirm.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
|
||||
<div class="app-info-title">
|
||||
{{ appPostInstallConfirm.app.manifest.title }}<br/>
|
||||
<span class="text-muted text-small">{{ 'app.appInfo.package' | tr }} <a ng-href="/#/appstore/{{appPostInstallConfirm.app.manifest.id}}?version={{appPostInstallConfirm.app.manifest.version}}">v{{ appPostInstallConfirm.app.manifest.version }}</a> </span>
|
||||
<br/>
|
||||
<span ng-show="appPostInstallConfirm.app.manifest.documentationUrl" class="text-small"><a target="_blank" ng-href="{{appPostInstallConfirm.app.manifest.documentationUrl}}">{{ 'app.docsAction' | tr }}</a> </span>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!--
|
||||
<p ng-show="appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.ssoEmail' | tr }}</p>
|
||||
<p ng-show="appPostInstallConfirm.app.sso && !appPostInstallConfirm.app.manifest.addons.email">{{ 'app.appInfo.sso' | tr }}</p>
|
||||
-->
|
||||
<div ng-bind-html="appPostInstallConfirm.message | markdown2html"></div>
|
||||
<div ng-show="appPostInstallConfirm.app.manifest.documentationUrl" ng-bind-html="'app.appInfo.appDocsUrl' | tr:{ docsUrl: appPostInstallConfirm.app.manifest.documentationUrl, title: appPostInstallConfirm.app.manifest.title, forumUrl: (appPostInstallConfirm.app.manifest.forumUrl || 'https://forum.cloudron.io') }"></div>
|
||||
|
||||
<div style="margin-top: 10px; margin-bottom: 5px;" ng-show="pendingChecklistItems(appPostInstallConfirm.app)">
|
||||
<label class="control-label">{{ 'app.appInfo.checklist' | tr }}</label>
|
||||
</div>
|
||||
<div ng-repeat="item in appPostInstallConfirm.app.checklist">
|
||||
<div class="checklist-item" ng-hide="item.acknowledged">
|
||||
<span ng-bind-html="item.message | markdown2html"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<a class="btn btn-success" ng-href="{{ 'https://' + appPostInstallConfirm.app.fqdn }}" target="_blank" ng-click="appPostInstallConfirm.submit()">{{ 'app.appInfo.openAction' | tr:{ app: appPostInstallConfirm.app.manifest.title } }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal applinks edit -->
|
||||
<div class="modal fade" id="applinksEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.editApplinkDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="applinksEditForm" role="form" ng-submit="applinksEdit.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (applinksEditForm.upstreamUri.$dirty && applinksEditForm.upstreamUri.$invalid) || (!applinksEditForm.upstreamUri.$dirty && applinksEdit.error.upstreamUri) }">
|
||||
<label class="control-label">{{ 'app.applinks.upstreamUri' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksEdit.upstreamUri" name="upstreamUri" id="inputUpstreamUri" autofocus autocomplete="off" required>
|
||||
<span class="text-danger" ng-show="applinksEdit.error.upstreamUri">{{ applinksEdit.error.upstreamUri }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.applinks.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksEdit.label" name="label" id="inputLabel" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'app.display.icon' | tr }}</label>
|
||||
</div>
|
||||
<div id="previewIcon" class="app-custom-icon" ng-click="applinksEdit.showCustomIconSelector()">
|
||||
<img ng-src="{{ applinksEdit.iconUrl() || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<a href="" style="font-weight: normal;" ng-click="applinksEdit.resetCustomIcon()">{{ 'app.applinks.clearIconAction' | tr }}</a> - <span class="text-small">{{ 'app.applinks.clearIconDescription' | tr }}</span>
|
||||
<input type="file" id="applinksEditIconFileInput" style="display: none" accept="image/png"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.display.tags' | tr }}</label>
|
||||
<tag-input class="form-control" placeholder="{{ 'app.display.tagsPlaceholder' | tr }}" taglist="applinksEdit.tags" name="tags" uib-tooltip="{{ 'app.display.tagsTooltip' | tr }}"></tag-input>
|
||||
</div>
|
||||
|
||||
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksEdit.accessRestrictionOption" value="any">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksEdit.accessRestrictionOption" value="groups">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="applinksEdit.accessRestrictionOption === 'groups' && !applinksEdit.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px; display: flex;">
|
||||
<div>
|
||||
{{ 'appstore.installDialog.users' | tr }}: <multiselect name="accessUsersSelect" class="input-sm stretch" ng-model="applinksEdit.accessRestriction.users" ng-disabled="applinksEdit.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in allUsers" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 'appstore.installDialog.groups' | tr }}: <multiselect name="accessGroupsSelect" class="input-sm stretch" ng-model="applinksEdit.accessRestriction.groups" ng-disabled="applinksEdit.accessRestrictionOption !== 'groups'" options="group.name for group in allGroups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="applinksEditForm.$invalid || applinksEdit.busyEdit || applinks.busyRemove"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger pull-left" ng-click="applinksEdit.remove()" ng-disabled="applinksEdit.busyRemove || applinksEdit.busyEdit"><i class="fa fa-circle-notch fa-spin" ng-show="applinksEdit.busyRemove"></i> {{ 'app.editApplinkDialog.deleteAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="applinksEdit.submit()" ng-disabled="applinksEditForm.$invalid || applinksEdit.busyRemove || applinksEdit.busyEdit"><i class="fa fa-circle-notch fa-spin" ng-show="applinksEdit.busyEdit"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content content-large">
|
||||
|
||||
<!-- Workaround for select-all issue, see commit message -->
|
||||
<div style="font-size: 1px;"> </div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1><i class="fa fa-cloud-download fa-fw"></i> {{ 'apps.noApps.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3 ng-bind-html="'apps.noApps.description' | tr:{ appStoreLink: '#/appstore' }"></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.isAtLeastAdmin">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1>{{ 'apps.noAccess.title' | tr }}</h1>
|
||||
<br/></br>
|
||||
<h3>{{ 'apps.noAccess.description' | tr }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="view-header" ng-show="installedApps.length > 0">
|
||||
{{ 'apps.title' | tr }}
|
||||
<div class="view-header-search-bar">
|
||||
<form class="form-inline">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="{{ 'apps.searchPlaceholder' | tr }} ( / )" id="appSearch" ng-model="appSearch"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-class="{ 'active': showFilter }" ng-click="toggleFilter()"><i class="fas fa-filter"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-default" style="margin-left: 10px;" type="button" ng-click="toggleView()"><i class="fas" ng-class="{ 'fa-list': view === VIEWS.GRID, 'fa-grip': view === VIEWS.LIST }"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div ng-show="showFilter" class="view-header-filter-bar">
|
||||
<form class="form-inline">
|
||||
<multiselect ng-model="selectedGroup" ng-show="user.isAtLeastAdmin && groups.length > 1" ms-header="{{ selectedGroup.name }}" options="group.name for group in groups" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedState" ng-show="user.isAtLeastAdmin" ms-header="{{ 'apps.stateFilterHeader' | tr }}" ms-selected="{{ selectedState }}" options="state.label for state in states" data-multiple="false"></multiselect>
|
||||
<multiselect ng-model="selectedTags" ng-show="user.isAtLeastAdmin && tags.length > 0" ms-header="{{ 'apps.tagsFilterHeaderAll' | tr }}" ms-selected="{{ 'apps.tagsFilterHeader' | tr:{ tags: selectedTags.join(', ') } }}" options="tag for tag in tags" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<multiselect ng-model="selectedDomain" data-compare-by="domain" ms-selected="{{ selectedDomain.domain }}" options="domain.domain for domain in filterDomains" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<!-- <button class="btn btn-primary" ng-disabled="!selectedTags.length && !selectedState.state && selectedGroup._unset && selectedDomain._alldomains" ng-click="clearAllFilter()">{{ 'apps.filter.clearAll' | tr }}</button> -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="app-grid" ng-show="view === VIEWS.GRID">
|
||||
<div class="grid-item" ng-class="{ 'stopped': app.runState === 'stopped' }" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:orderByFilter">
|
||||
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}" tooltip-append-to-body="true">
|
||||
<a ng-show="app.type !== APP_TYPES.LINK && isOperator(app)" ng-href="#/app/{{ app.id}}/info" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
|
||||
<div ng-show="app.type === APP_TYPES.LINK && isOperator(app)" ng-click="applinksEdit.show(app)" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></div>
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title" data-fittext>{{ app.label || app.subdomain || app.fqdn }}</div>
|
||||
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div class="status" ng-style="{ 'visibility': isOperator(app) && (app | installationActive) ? 'visible' : 'hidden' }">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usermanagement-indicator" ng-show="app.type !== APP_TYPES.LINK">
|
||||
<i class="fa-brands fa-openid" ng-show="app.ssoAuth && app.manifest.addons.oidc" uib-tooltip="{{ 'apps.auth.openid' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && (!app.manifest.addons.oidc && !app.manifest.addons.email)" uib-tooltip="{{ 'apps.auth.sso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}" tooltip-placement="right"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}" tooltip-placement="right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<!-- update info is available to app users. but we should show update indicator only for operators since normal users cannot update -->
|
||||
<div class="app-update-badge" ng-click="showAppConfigure(app, 'updates')" ng-show="isOperator(app) && config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')" uib-tooltip="Update Available">
|
||||
<i class="fa fa-arrow-up fa-inverse"></i>
|
||||
</div>
|
||||
|
||||
<div class="app-checklist-badge" ng-click="showAppConfigure(app, 'info')" ng-show="pendingChecklistItems(app)">
|
||||
{{ pendingChecklistItems(app) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-list card card-large" ng-show="view === VIEWS.LIST">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 32px" class="hand" ng-click="setOrderBy('status')"><i ng-show="orderBy === 'status'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 32px"> </th>
|
||||
<th style="width: 35%" class="hand" ng-click="setOrderBy('location')">{{ 'app.display.label' | tr }} <i ng-show="orderBy === 'location'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 30%" class="hand hide-mobile" ng-click="setOrderBy('app')">App Title<i ng-show="orderBy === 'app'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width: 32px" class="hide-mobile"> </th>
|
||||
<th style="width: 32px" class="hand hide-mobile text-center" ng-click="setOrderBy('sso')"><i class="fas fa-user-lock"></i> <i ng-show="orderBy === 'sso'" class="fas fa-arrow-{{ orderByReverse ? 'up' : 'down' }}-long"></i></th>
|
||||
<th style="width:160px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="app-list-item" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:orderByFilter:orderByReverse" uib-tooltip="{{ app | appProgressMessage }}">
|
||||
<td class="elide-table-cell">
|
||||
<i class="fa fa-circle" ng-class="app | installationStateClass" uib-tooltip="{{ app | installationStateLabel }}"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell app-list-app-link-cell">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank" class="app-list-app-link">
|
||||
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-list-item-icon"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="elide-table-cell app-list-app-link-cell">
|
||||
<a ng-href="{{ app | applicationLink }}" ng-click="onAppClick(app, $event)" target="_blank" class="app-list-app-link">
|
||||
<span style="font-size: 16px;">{{ app.label || app.subdomain || app.fqdn }}</span><br/>
|
||||
<span class="text-muted text-small">{{ app.fqdn.indexOf('http') === 0 ? app.fqdn : 'https://'+app.fqdn }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="elide-table-cell hide-mobile">{{ app.manifest.title || 'App Link' }}</td>
|
||||
<td class="elide-table-cell hide-mobile text-center">
|
||||
<a class="badge badge-danger" ng-show="pendingChecklistItems(app)" ng-href="#/app/{{ app.id}}/info">{{ pendingChecklistItems(app) }}</a>
|
||||
</td>
|
||||
<td class="elide-table-cell hide-mobile text-center">
|
||||
<div ng-show="app.type !== APP_TYPES.LINK">
|
||||
<i class="fa-brands fa-openid" ng-show="app.ssoAuth && app.manifest.addons.oidc" uib-tooltip="{{ 'apps.auth.openid' | tr }}"></i>
|
||||
<i class="fas fa-user" ng-show="app.ssoAuth && (!app.manifest.addons.oidc && !app.manifest.addons.email)" uib-tooltip="{{ 'apps.auth.sso' | tr }}"></i>
|
||||
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}"></i>
|
||||
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right" style="vertical-align: middle; white-space: nowrap;">
|
||||
<span ng-show="isOperator(app)">
|
||||
<a class="btn btn-xs btn-success" style="padding: 1px 7px;" ng-show="config.update[app.id].manifest.version && config.update[app.id].manifest.version !== app.manifest.version && (app | installSuccess) && !(app.error || app.runState === 'stopped')" ng-href="#/app/{{ app.id}}/updates" uib-tooltip="Update Available"><i class="fa fa-arrow-up"></i></a>
|
||||
|
||||
<div class="btn-group btn-group-xs" role="group">
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.LINK" ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}"><i class="fas fa-align-left"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.PROXIED && app.type !== APP_TYPES.LINK" ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}"><i class="fa fa-terminal"></i></a>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.manifest.addons.localstorage" ng-href="{{ '/filemanager.html#/home/app/' + app.id }}" target="_blank" tooltip-append-to-body="true" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-xs btn-default" ng-show="app.type === APP_TYPES.LINK" ng-click="applinksEdit.show(app)" uib-tooltip="Configure Applink"><i class="fa fa-cog"></i></button>
|
||||
<a class="btn btn-xs btn-default" ng-show="app.type !== APP_TYPES.LINK" ng-href="#/app/{{ app.id}}/info" uib-tooltip="Configure App"><i class="fa fa-cog"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<div>
|
||||
{{ 'apps.apps.count' | tr:{ count: (installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch).length } }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,389 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
/* global APP_TYPES */
|
||||
/* global onAppClick */
|
||||
|
||||
angular.module('Application').controller('AppsController', ['$scope', '$translate', '$interval', '$location', 'Client', function ($scope, $translate, $interval, $location, Client) {
|
||||
var ALL_DOMAINS_DOMAIN = { _alldomains: true, domain: 'All Domains' }; // dummy record for the single select filter
|
||||
var GROUP_ACCESS_UNSET = { _unset: true, name: 'Select Group' }; // dummy record for the single select filter
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.tags = Client.getAppTags();
|
||||
$scope.states = [
|
||||
{ state: '', label: 'All States' },
|
||||
{ state: 'running', label: 'Running' },
|
||||
{ state: 'stopped', label: 'Stopped' },
|
||||
{ state: 'update_available', label: 'Update Available' },
|
||||
{ state: 'not_responding', label: 'Not Responding' }
|
||||
];
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
$scope.filterDomains = [ ALL_DOMAINS_DOMAIN ];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.domains = [];
|
||||
$scope.appSearch = '';
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ];
|
||||
$scope.APP_TYPES = APP_TYPES;
|
||||
$scope.showFilter = false;
|
||||
$scope.filterActive = false;
|
||||
|
||||
$scope.VIEWS = {
|
||||
GRID: 'grid',
|
||||
LIST: 'list'
|
||||
};
|
||||
$scope.view = $scope.VIEWS.GRID;
|
||||
|
||||
$scope.orderBy = 'location'; // or app, status, sso
|
||||
$scope.orderByReverse = false;
|
||||
|
||||
$scope.allUsers = [];
|
||||
$scope.allGroups = [];
|
||||
|
||||
$translate(['apps.stateFilterHeader', 'apps.domainsFilterHeader', 'apps.groupsFilterHeader', 'app.states.running', 'app.states.stopped', 'app.states.notResponding', 'app.states.updateAvailable']).then(function (tr) {
|
||||
if (tr['apps.domainsFilterHeader']) ALL_DOMAINS_DOMAIN.domain = tr['apps.domainsFilterHeader'];
|
||||
if (tr['apps.groupsFilterHeader']) GROUP_ACCESS_UNSET.name = tr['apps.groupsFilterHeader'];
|
||||
if (tr['apps.stateFilterHeader']) $scope.states[0].label = tr['apps.stateFilterHeader'];
|
||||
if (tr['app.states.running']) $scope.states[1].label = tr['app.states.running'];
|
||||
if (tr['app.states.stopped']) $scope.states[2].label = tr['app.states.stopped'];
|
||||
if (tr['app.states.notResponding']) $scope.states[4].label = tr['app.states.notResponding'];
|
||||
if (tr['app.states.updateAvailable']) $scope.states[3].label = tr['app.states.updateAvailable'];
|
||||
});
|
||||
|
||||
$scope.pendingChecklistItems = function (app) {
|
||||
if (!app.checklist) return 0;
|
||||
return Object.keys(app.checklist).filter(function (key) { return !app.checklist[key].acknowledged; }).length;
|
||||
};
|
||||
|
||||
$scope.setOrderBy = function (by) {
|
||||
if (by === $scope.orderBy) {
|
||||
$scope.orderByReverse = !$scope.orderByReverse;
|
||||
} else {
|
||||
$scope.orderBy = by;
|
||||
$scope.orderByReverse = false;
|
||||
}
|
||||
|
||||
localStorage.appsOrderBy = by;
|
||||
if ($scope.orderByReverse) localStorage.appsOrderByReverse = true;
|
||||
else localStorage.removeItem('appsOrderByReverse');
|
||||
};
|
||||
|
||||
// for sorting/grouping
|
||||
$scope.orderByFilter = function (item) {
|
||||
if ($scope.orderBy === 'app') return item.manifest.title || 'App Link';
|
||||
if ($scope.orderBy === 'status') return item.installationState + '-' + item.runState;
|
||||
if ($scope.orderBy === 'sso') {
|
||||
if (item.ssoAuth && item.manifest.addons.oidc) return 'oidc';
|
||||
if (item.ssoAuth && (!item.manifest.addons.oidc && !item.manifest.addons.email)) return 'sso';
|
||||
if (item.manifest.addons.email) return 'email';
|
||||
return '';
|
||||
}
|
||||
return item.label || item.fqdn;
|
||||
};
|
||||
|
||||
$scope.setView = function (view) {
|
||||
if (view !== $scope.VIEWS.LIST && view !== $scope.VIEWS.GRID) return;
|
||||
|
||||
$scope.view = view;
|
||||
localStorage.appsView = view;
|
||||
};
|
||||
|
||||
$scope.toggleView = function () {
|
||||
$scope.view = $scope.view === $scope.VIEWS.GRID ? $scope.VIEWS.LIST : $scope.VIEWS.GRID;
|
||||
localStorage.appsView = $scope.view;
|
||||
};
|
||||
|
||||
$scope.toggleFilter = function () {
|
||||
$scope.showFilter = !$scope.showFilter;
|
||||
|
||||
if ($scope.showFilter) localStorage.appsShowFilter = true;
|
||||
else localStorage.removeItem('appsShowFilter');
|
||||
|
||||
// clear on hide
|
||||
if (!$scope.showFilter) {
|
||||
$scope.selectedState = $scope.states[0];
|
||||
$scope.selectedTags = [];
|
||||
$scope.selectedGroup = GROUP_ACCESS_UNSET;
|
||||
$scope.selectedDomain = ALL_DOMAINS_DOMAIN;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('selectedTags', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
localStorage.selectedTags = newVal.join(',');
|
||||
});
|
||||
|
||||
$scope.$watch('selectedState', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === $scope.states[0]) localStorage.removeItem('selectedState');
|
||||
else localStorage.selectedState = newVal.state;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedGroup', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal === GROUP_ACCESS_UNSET) localStorage.removeItem('selectedGroup');
|
||||
else localStorage.selectedGroup = newVal.id;
|
||||
});
|
||||
|
||||
$scope.$watch('selectedDomain', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
if (newVal._alldomains) localStorage.removeItem('selectedDomain');
|
||||
else localStorage.selectedDomain = newVal.domain;
|
||||
});
|
||||
|
||||
$scope.onAppClick = function (app, $event) { onAppClick(app, $event, $scope.isOperator(app), $scope); };
|
||||
|
||||
$scope.appPostInstallConfirm = {
|
||||
app: {},
|
||||
message: '',
|
||||
|
||||
show: function (app) {
|
||||
$scope.appPostInstallConfirm.app = app;
|
||||
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
|
||||
|
||||
$('#appsPostInstallConfirmModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
|
||||
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
|
||||
|
||||
$('#appsPostInstallConfirmModal').modal('hide');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.applinksEdit = {
|
||||
error: {},
|
||||
busyEdit: false,
|
||||
busyRemove: false,
|
||||
applink: {},
|
||||
id: '',
|
||||
upstreamUri: '',
|
||||
label: '',
|
||||
tags: '',
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
icon: { data: null },
|
||||
|
||||
iconUrl: function () {
|
||||
if ($scope.applinksEdit.icon.data === '__original__') { // user clicked reset
|
||||
// https://png-pixel.com/ white pixel placeholder
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=';
|
||||
} else if ($scope.applinksEdit.icon.data) { // user uploaded icon
|
||||
return $scope.applinksEdit.icon.data;
|
||||
} else { // current icon
|
||||
return $scope.applinksEdit.applink.iconUrl;
|
||||
}
|
||||
},
|
||||
|
||||
resetCustomIcon: function () {
|
||||
$scope.applinksEdit.icon.data = '__original__';
|
||||
},
|
||||
|
||||
showCustomIconSelector: function () {
|
||||
$('#applinksEditIconFileInput').click();
|
||||
},
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
return !!($scope.applinksEdit.accessRestriction.users.length || $scope.applinksEdit.accessRestriction.groups.length);
|
||||
},
|
||||
|
||||
show: function (applink) {
|
||||
$scope.applinksEdit.error = {};
|
||||
$scope.applinksEdit.busyEdit = false;
|
||||
$scope.applinksEdit.busyRemove = false;
|
||||
$scope.applinksEdit.applink = applink;
|
||||
$scope.applinksEdit.id = applink.id;
|
||||
$scope.applinksEdit.upstreamUri = applink.upstreamUri;
|
||||
$scope.applinksEdit.label = applink.label;
|
||||
$scope.applinksEdit.accessRestrictionOption = applink.accessRestriction ? 'groups' : 'any';
|
||||
$scope.applinksEdit.accessRestriction = { users: [], groups: [] };
|
||||
$scope.applinksEdit.icon = { data: null };
|
||||
|
||||
var userSet, groupSet;
|
||||
if (applink.accessRestriction) {
|
||||
userSet = {};
|
||||
applink.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
||||
$scope.allUsers.forEach(function (u) { if (userSet[u.id] === true) $scope.applinksEdit.accessRestriction.users.push(u); });
|
||||
|
||||
groupSet = {};
|
||||
if (applink.accessRestriction.groups) applink.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
||||
$scope.allGroups.forEach(function (g) { if (groupSet[g.id] === true) $scope.applinksEdit.accessRestriction.groups.push(g); });
|
||||
}
|
||||
|
||||
// translate for tag-input
|
||||
$scope.applinksEdit.tags = applink.tags ? applink.tags.join(' ') : '';
|
||||
|
||||
$scope.applinksEditForm.$setUntouched();
|
||||
$scope.applinksEditForm.$setPristine();
|
||||
|
||||
$('#applinksEditModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.applinksEdit.busyEdit = true;
|
||||
$scope.applinksEdit.error = {};
|
||||
|
||||
var accessRestriction = null;
|
||||
if ($scope.applinksEdit.accessRestrictionOption === 'groups') {
|
||||
accessRestriction = { users: [], groups: [] };
|
||||
accessRestriction.users = $scope.applinksEdit.accessRestriction.users.map(function (u) { return u.id; });
|
||||
accessRestriction.groups = $scope.applinksEdit.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
upstreamUri: $scope.applinksEdit.upstreamUri,
|
||||
label: $scope.applinksEdit.label,
|
||||
accessRestriction: accessRestriction,
|
||||
tags: $scope.applinksEdit.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; })
|
||||
};
|
||||
|
||||
if ($scope.applinksEdit.icon.data === '__original__') { // user reset the icon
|
||||
data.icon = '';
|
||||
} else if ($scope.applinksEdit.icon.data) { // user loaded custom icon
|
||||
data.icon = $scope.applinksEdit.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
}
|
||||
|
||||
Client.updateApplink($scope.applinksEdit.id, data, function (error) {
|
||||
$scope.applinksEdit.busyEdit = false;
|
||||
|
||||
if (error && error.statusCode === 400 && error.message.includes('upstreamUri')) {
|
||||
$scope.applinksEdit.error.upstreamUri = error.message;
|
||||
$scope.applinksEditForm.$setUntouched();
|
||||
$scope.applinksEditForm.$setPristine();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Failed to update applink', error);
|
||||
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#applinksEditModal').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
$scope.applinksEdit.busyRemove = true;
|
||||
|
||||
Client.removeApplink($scope.applinksEdit.id, function (error) {
|
||||
$scope.applinksEdit.busyRemove = false;
|
||||
|
||||
if (error) return console.error('Failed to remove applink', error);
|
||||
|
||||
Client.refreshInstalledApps();
|
||||
|
||||
$('#applinksEditModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showAppConfigure = function (app, view) {
|
||||
$location.path('/app/' + app.id + '/' + view);
|
||||
};
|
||||
|
||||
$scope.isOperator = function (app) {
|
||||
return app.accessLevel === 'operator' || app.accessLevel === 'admin';
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
setTimeout(function () { $('#appSearch').focus(); }, 1);
|
||||
|
||||
// refresh the new list immediately when switching from another view (appstore)
|
||||
Client.refreshInstalledApps(function () {
|
||||
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client, function () {}), 5000);
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshAppsTimer);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.setView(localStorage.appsView);
|
||||
|
||||
$scope.orderBy = localStorage.appsOrderBy || 'location';
|
||||
$scope.orderByReverse = !!localStorage.appsOrderByReverse;
|
||||
$scope.showFilter = !!localStorage.appsShowFilter;
|
||||
|
||||
if (!$scope.user.isAtLeastAdmin) return;
|
||||
|
||||
// load local settings and apply tag filter
|
||||
if (localStorage.selectedTags) {
|
||||
if (!$scope.tags.length) localStorage.removeItem('selectedTags');
|
||||
else $scope.selectedTags = localStorage.selectedTags.split(',');
|
||||
}
|
||||
|
||||
if (localStorage.selectedState) $scope.selectedState = $scope.states.find(function (s) { return s.state === localStorage.selectedState; }) || $scope.states[0];
|
||||
|
||||
Client.getGroups(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.groups = [ GROUP_ACCESS_UNSET ].concat(result);
|
||||
$scope.allGroups = result;
|
||||
|
||||
if (localStorage.selectedGroup) $scope.selectedGroup = $scope.groups.find(function (g) { return g.id === localStorage.selectedGroup; }) || GROUP_ACCESS_UNSET;
|
||||
});
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.domains = result;
|
||||
$scope.filterDomains = [ALL_DOMAINS_DOMAIN].concat(result);
|
||||
|
||||
if (localStorage.selectedDomain) $scope.selectedDomain = $scope.filterDomains.find(function (d) { return d.domain === localStorage.selectedDomain; }) || ALL_DOMAINS_DOMAIN;
|
||||
});
|
||||
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.allUsers = users;
|
||||
});
|
||||
});
|
||||
|
||||
$('#applinksEditIconFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
// var file = event.target.files[0];
|
||||
$scope.applinksEdit.icon.data = fr.result;
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['applinksAddModal', 'applinksEditModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.collapse').on('shown.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-right').removeClass('fa-angle-right').addClass('fa-angle-down');
|
||||
}).on('hidden.bs.collapse', function(){
|
||||
$(this).parent().find('.fa-angle-down').removeClass('fa-angle-down').addClass('fa-angle-right');
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
|
||||
function keyboardHandler(event) {
|
||||
if (event.key === '/') {
|
||||
document.getElementById('appSearch').focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyboardHandler);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
document.removeEventListener('keydown', keyboardHandler);
|
||||
});
|
||||
}]);
|
||||
@@ -1,481 +0,0 @@
|
||||
|
||||
<!-- Modal install app -->
|
||||
<div class="modal fade appstore-install" id="appInstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null; this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
<h3 class="appstore-install-title">{{ appInstall.app.manifest.title }}</h3>
|
||||
<br/>
|
||||
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">{{ appInstall.app.manifest.author }}</a></span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.lastUpdated' | tr:{ date: (appInstall.app.creationDate | prettyDate) } }}</span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ 'appstore.installDialog.memoryRequirement' | tr:{ size: (appInstall.app.manifest.memoryLimit | prettyBinarySize:'256 MB') } }}</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse" id="collapseInstallForm" data-toggle="false">
|
||||
<form role="form" name="appInstallForm" ng-submit="appInstall.submit()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appInstall.error.other" ng-bind-html="appInstall.error.other"></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appInstallForm.location.$dirty && appInstallForm.location.$invalid) || (!appInstallForm.location.$dirty && appInstall.error.location) }">
|
||||
<label class="control-label" for="appInstallLocationInput">{{ 'appstore.installDialog.location' | tr }}</label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.subdomain" id="appInstallLocationInput" name="location" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ '.' + appInstall.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="appInstall.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="appInstall.error.location" class="text-small">{{ appInstall.error.location }}</div>
|
||||
</div>
|
||||
|
||||
<p class="text-small text-warning" ng-show="appInstall.domain.provider === 'noop' || appInstall.domain.provider === 'manual'" ng-bind-html="'appstore.installDialog.manualWarning' | tr:{ location: ((appInstall.subdomain ? appInstall.subdomain + '.' : '') + appInstall.domain.domain) }"></p>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.secondaryDomain">{{ appInstall.error.secondaryDomain }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.app.manifest.httpPorts">
|
||||
<ng-form name="secondaryDomainInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!secondaryDomainInfo_form.itemName{{$index}}.$dirty && appInstall.error.secondaryDomain) || (secondaryDomainInfo_form.itemName{{$index}}.$dirty && secondaryDomainInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="secondaryDomainInput{{env}}">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
</label>
|
||||
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.secondaryDomains[env].subdomain" name="location{{$index}}" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>.{{ appInstall.secondaryDomains[env].domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="appInstall.secondaryDomains[env].domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.portInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}. {{info.portCount >=1 ? (info.portCount + ' ports. ') : ''}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="appInstall.ports[env]" ng-disabled="!appInstall.portsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<p class="text-small text-warning text-bold" ng-show="appInstall.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="isProxyApp(appInstall.app)">
|
||||
<label class="control-label" for="appInstallUpstreamUriInput">Upstream URI</label>
|
||||
<input type="text" class="form-control" ng-model="appInstall.upstreamUri" id="appInstallUpstreamUriInput" name="upstreamUri" ng-required="isProxyApp(appInstall.app)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="appInstall.app.manifest.addons.email">
|
||||
<label class="control-label">{{ 'appstore.installDialog.userManagement' | tr }}</label>
|
||||
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
|
||||
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" ng-show="!appInstall.customAuth && !appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagement' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label class="control-label" ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<p ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagementNone' | tr }}</p>
|
||||
<div class="radio" ng-show="appInstall.optionalSso">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="nosso"> {{ 'appstore.installDialog.userManagementLeaveToApp' | tr }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="any">
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementAllUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="groups">
|
||||
<span ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagementSelectUsers' | tr }}</span>
|
||||
<span ng-show="appInstall.customAuth">{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px;">
|
||||
<div class="col-md-5">
|
||||
{{ 'appstore.installDialog.users' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.users" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
{{ 'appstore.installDialog.groups' | tr }}:
|
||||
<multiselect ng-model="appInstall.accessRestriction.groups" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="collapse" id="collapseMediaLinksCarousel" data-toggle="false">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');" ng-show="appInstall.mediaLinks.length == 1"></div>
|
||||
<slick init-onload="true" current-index="0" autoplay="true" arrows="false" autoplay-speed="2000" data="appInstall.mediaLinks" ng-show="appInstall.mediaLinks.length > 1">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');"></div>
|
||||
</slick>
|
||||
<br/>
|
||||
<div class="appstore-install-description">
|
||||
<p ng-show="appInstall.app.manifest.upstreamVersion">{{ 'appstore.installDialog.titleAndVersion' | tr:{ title: appInstall.app.manifest.title, version: appInstall.app.manifest.upstreamVersion } }}</p>
|
||||
<div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
|
||||
<h4 class="text-danger">{{ 'appstore.installDialog.lowOnResources' | tr }}<sup><a ng-href="https://docs.cloudron.io/apps/#low-resource-warning" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
<p>{{ 'appstore.installDialog.pleaseUpgradeServer' | tr }}</p>
|
||||
</div>
|
||||
<div class="collapse" id="collapseSubscriptionRequired" data-toggle="false">
|
||||
<p>{{ 'appstore.installDialog.subscriptionRequired' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()" ng-show="appInstall.state === 'subscriptionRequired'">{{ 'appstore.installDialog.setupSubscriptionAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">{{ 'appstore.installDialog.installAnywayAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'appInfo'" ng-click="appInstall.showForm()">{{ 'appstore.installDialog.installAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" ng-click="appInstall.submit()" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || !appInstall.accessRestrictionOption || appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="appInstall.busy"></i> {{ 'appstore.installDialog.doInstallAction' | tr:{ dnsOverwrite: appInstall.needsOverwrite } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal app not found -->
|
||||
<div class="modal fade" id="appNotFoundModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'appstore.appNotFoundDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-bind-html="'appstore.appNotFoundDialog.description' | tr:{ appId: appNotFound.appId, version: appNotFound.version }"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal applinks add -->
|
||||
<div class="modal fade" id="applinksAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'app.addApplinkDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="applinksAddForm" role="form" ng-submit="applinksAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (applinksAddForm.upstreamUri.$dirty && applinksAddForm.upstreamUri.$invalid) || (!applinksAddForm.upstreamUri.$dirty && applinksAdd.error.upstreamUri) }">
|
||||
<label class="control-label">{{ 'app.applinks.upstreamUri' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksAdd.upstreamUri" name="upstreamUri" id="inputUpstreamUri" autofocus autocomplete="off" required>
|
||||
<span class="text-danger" ng-show="applinksAdd.error.upstreamUri">{{ applinksAdd.error.upstreamUri }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.applinks.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="applinksAdd.label" name="label" id="inputLabel" autocomplete="off" placeholder="Leave empty for autodetection">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'app.display.tags' | tr }}</label>
|
||||
<tag-input class="form-control" placeholder="{{ 'app.display.tagsPlaceholder' | tr }}" taglist="applinksAdd.tags" name="tags" uib-tooltip="{{ 'app.display.tagsTooltip' | tr }}"></tag-input>
|
||||
</div>
|
||||
|
||||
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksAdd.accessRestrictionOption" value="any">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="applinksAdd.accessRestrictionOption" value="groups">
|
||||
<span>{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
|
||||
<span class="label label-danger" ng-show="applinksAdd.accessRestrictionOption === 'groups' && !applinksAdd.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-left: 20px; display: flex;">
|
||||
<div>
|
||||
{{ 'appstore.installDialog.users' | tr }}: <multiselect name="accessUsersSelect" class="input-sm stretch" ng-model="applinksAdd.accessRestriction.users" ng-disabled="applinksAdd.accessRestrictionOption !== 'groups'" options="(user.username || user.email) for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 'appstore.installDialog.groups' | tr }}: <multiselect name="accessGroupsSelect" class="input-sm stretch" ng-model="applinksAdd.accessRestriction.groups" ng-disabled="applinksAdd.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="applinksAddForm.$invalid || applinksAdd.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="applinksAdd.submit()" ng-disabled="applinksAddForm.$invalid || applinksAdd.busy"><i class="fa fa-circle-notch fa-spin" ng-show="applinksAdd.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!ready" class="loading-banner">
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
</div>
|
||||
|
||||
<!-- appstore login -->
|
||||
<div ng-show="ready && !validSubscription" class="container card card-small appstore-login ng-cloak">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1 ng-show="appstoreLogin.setupType === 'signup'">{{ 'appstore.accountDialog.titleSignUp' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.setupType === 'login'">{{ 'appstore.accountDialog.titleLogin' | tr }}</h1>
|
||||
<h1 ng-show="appstoreLogin.setupType === 'setupToken'">{{ 'appstore.accountDialog.titleToken' | tr }}</h1>
|
||||
</div>
|
||||
<div class="col-md-12 text-center">
|
||||
<p>{{ 'appstore.accountDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.generic">{{ appstoreLogin.error.generic }}</small>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'signup'">
|
||||
<form name="appstoreSignupForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreSignupForm.email.$dirty && appstoreSignupForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" id="inputAppstoreSignupEmail" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreSignupForm.email.$dirty && appstoreLogin.error.email) || (appstoreSignupForm.email.$dirty && appstoreSignupForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword) || (appstoreSignupForm.password.$dirty && appstoreSignupForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreSignupPassword" name="password" required password-reveal>
|
||||
<div class="control-label" ng-show="(!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword) || (appstoreSignupForm.password.$dirty && appstoreSignupForm.password.$invalid)">
|
||||
<small ng-show="!appstoreSignupForm.password.$dirty && appstoreLogin.error.signupPassword">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreSignupForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.createAccountAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'login'">
|
||||
<form name="appstoreLoginForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.email' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="appstoreLogin.email" name="email" required autofocus>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.email.$dirty && appstoreLogin.error.email) || (appstoreLoginForm.email.$dirty && appstoreLoginForm.email.$invalid) || appstoreLogin.error.email">
|
||||
<small class="text-danger" ng-show="appstoreLogin.error.email">{{ appstoreLogin.error.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid) || appstoreLogin.error.generic }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.password' | tr }}</label>
|
||||
<input type="password" class="form-control" ng-model="appstoreLogin.password" id="inputAppstoreLoginPassword" name="password" required password-reveal>
|
||||
<div class="control-label" ng-show="(!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword) || (appstoreLoginForm.password.$dirty && appstoreLoginForm.password.$invalid)">
|
||||
<small ng-show="!appstoreLoginForm.password.$dirty && appstoreLogin.error.loginPassword">{{ 'appstore.accountDialog.errorWrongPassword' | tr }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': appstoreLogin.error.totpToken }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.2faToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="appstoreLogin.totpToken" id="inputAppstoreLoginTotpToken" name="totpToken">
|
||||
<div class="control-label" ng-show="appstoreLogin.error.totpToken">
|
||||
<small ng-show="appstoreLogin.error.totpToken">{{ appstoreLogin.error.totpToken }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.loginAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="appstoreLogin.setupType === 'setupToken'">
|
||||
<form name="appstoreSetupTokenForm" role="form" novalidate ng-submit="appstoreLogin.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': appstoreLogin.error.setupToken }">
|
||||
<label class="control-label">{{ 'appstore.accountDialog.setupToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="appstoreLogin.setupToken" id="inputAppstoreSetupToken" name="setupToken" ng-required="true">
|
||||
<div class="control-label" ng-show="appstoreLogin.error.setupToken">
|
||||
<small ng-show="appstoreLogin.error.setupToken">{{ appstoreLogin.error.setupToken }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="appstoreLogin.termsAccepted" ng-required="true"><span ng-bind-html="'appstore.accountDialog.licenseCheckbox' | tr:{ licenseLink: 'https://cloudron.io/legal/license.html' }"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreSetupTokenForm.$invalid || appstoreLogin.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="appstoreLogin.busy"></i> {{ 'appstore.accountDialog.setupWithTokenAction' | tr }}
|
||||
</button>
|
||||
</center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'signup'" ng-show="appstoreLogin.setupType === 'login'">{{ 'appstore.accountDialog.switchToSignUpAction' | tr }}</a>
|
||||
<a href="" ng-click="appstoreLogin.setupType = 'login'" ng-show="appstoreLogin.setupType === 'signup' || appstoreLogin.setupType === 'setupToken'">{{ 'appstore.accountDialog.switchToLoginAction' | tr }}</a>
|
||||
<span ng-show="appstoreLogin.setupType !== 'setupToken'"> or <a href="" ng-click="appstoreLogin.setupType = 'setupToken'">Use a setup token</a></span>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- give more vertical spacing so the login form does not appear clipped -->
|
||||
<div ng-show="ready && !validSubscription">
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="appstore-layout">
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-toolbar">
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" ng-class="{ 'btn-primary': '' !== category && 'recent' !== category && 'new' !== category }">
|
||||
{{ categoryButtonLabel(category) }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="showCategory('');"><i class="fas fa-home fa-fw"></i> {{ 'appstore.category.all' | tr }}</a></li>
|
||||
<li><a href="" ng-click="showCategory('new');"><i class="fas fa-rss fa-fw"></i> {{ 'appstore.category.newApps' | tr }}</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li ng-repeat="category in categories | orderBy:'label'"><a href="" ng-click="showCategory(category.id);"><i class="{{ category.icon }} fa-fw"></i> {{ category.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<i class="{{ userManagementFilterOption.icon }} fa-fw"></i>
|
||||
{{ 'appstore.ssofilter.label' | tr }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="option in userManagementFilterOptions" ng-class="{ 'active': userManagementFilterOption.id && userManagementFilterOption.id === option.id }"><a href="" ng-click="applyUserMangamentFilter(option);"><i class="{{ option.icon }} fa-fw"></i> {{ option.label }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="text" id="appstoreSearch" class="form-control" style="width: auto; flex-grow: 1;" placeholder="{{ 'appstore.searchPlaceholder' | tr }}" ng-model="searchString" ng-change="search()" autofocus>
|
||||
<button type="button" class="btn btn-default" ng-click="openAppProxy()"><i class="fas fa-exchange-alt"></i> {{ 'apps.addAppproxyAction' | tr }}</a></a>
|
||||
<button type="button" class="btn btn-default" ng-click="applinksAdd.show()"><i class="fas fa-link"></i> {{ 'apps.addApplinkAction' | tr }}</button>
|
||||
</div>
|
||||
|
||||
<div ng-show="ready && validSubscription" class="ng-cloak appstore-grid">
|
||||
<div class="text-center" ng-hide="apps.length || popularApps.length">
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<h3 class="text-muted">{{ 'appstore.noAppsFound' | tr }}</h3>
|
||||
<br/>
|
||||
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank">{{ 'appstore.appMissing' | tr }}</a>
|
||||
</div>
|
||||
<div class="" ng-show="category === '' && popularApps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.popular' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-3 appstore-item" ng-repeat="app in popularApps | userManagementFilter:userManagementFilterOption">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="" ng-show="apps.length">
|
||||
<div class="row-no-margin" ng-show="!category && !searchString">
|
||||
<div class="col-sm-12">
|
||||
<h2>{{ 'appstore.category.all' | tr }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-3 appstore-item" ng-repeat="app in apps | userManagementFilter:userManagementFilterOption | orderBy:'-priority' ">
|
||||
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="appstore-item-content-description col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,787 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global async */
|
||||
/* global ERROR */
|
||||
/* global RSTATES */
|
||||
/* global moment */
|
||||
|
||||
angular.module('Application').controller('AppStoreController', ['$scope', '$translate', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $translate, $location, $timeout, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.HOST_PORT_MIN = 1;
|
||||
$scope.HOST_PORT_MAX = 65535;
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.apps = [];
|
||||
$scope.popularApps = [];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.users = [];
|
||||
$scope.groups = [];
|
||||
$scope.domains = [];
|
||||
$scope.category = '';
|
||||
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
||||
$scope.searchString = '';
|
||||
$scope.validSubscription = false;
|
||||
$scope.subscription = {};
|
||||
$scope.memory = null; // { memory, swap }
|
||||
|
||||
$scope.showView = function (view) {
|
||||
$('#appInstallModal').off('hidden.bs.modal');
|
||||
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
$('.modal').on('hidden.bs.modal', function () {
|
||||
$scope.appInstall.reset();
|
||||
$('.modal').off('hidden.bs.modal');
|
||||
$location.path(view).search({});
|
||||
});
|
||||
|
||||
$('.modal').modal('hide');
|
||||
};
|
||||
|
||||
// If new categories added make sure the translation below exists
|
||||
$scope.categories = [
|
||||
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
|
||||
{ id: 'automation', icon: 'fa fa-robot', label: 'Automation'},
|
||||
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
|
||||
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
|
||||
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
|
||||
{ id: 'document', icon: 'fa fa-file-word', label: 'Documents'},
|
||||
{ id: 'email', icon: 'fa fa-envelope', label: 'Email'},
|
||||
{ id: 'federated', icon: 'fa fa-project-diagram', label: 'Federated'},
|
||||
{ id: 'finance', icon: 'fa fa-hand-holding-usd', label: 'Finance'},
|
||||
{ id: 'forum', icon: 'fa fa-users', label: 'Forum'},
|
||||
{ id: 'fun', icon: 'fa fa-gamepad', label: 'Fun'},
|
||||
{ id: 'gallery', icon: 'fa fa-images', label: 'Gallery'},
|
||||
{ id: 'game', icon: 'fa fa-gamepad', label: 'Games'},
|
||||
{ id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
|
||||
{ id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
|
||||
{ id: 'learning', icon: 'fas fa-graduation-cap', label: 'Learning'},
|
||||
{ id: 'media', icon: 'fas fa-photo-video', label: 'Media'},
|
||||
{ id: 'no-code', icon: 'fas fa-code', label: 'No-code'},
|
||||
{ id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
|
||||
{ id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
|
||||
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
|
||||
{ id: 'voip', icon: 'fa fa-headset', label: 'VoIP'},
|
||||
{ id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
|
||||
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
|
||||
];
|
||||
|
||||
// Translation IDs are generated as "appstore.category.<categoryId>"
|
||||
$translate($scope.categories.map(function (c) { return 'appstore.category.' + c.id; })).then(function (tr) {
|
||||
Object.keys(tr).forEach(function (key) {
|
||||
if (key === tr[key]) return; // missing translation use default label
|
||||
|
||||
var category = $scope.categories.find(function (c) { return key.endsWith(c.id); });
|
||||
if (category) category.label = tr[key];
|
||||
});
|
||||
});
|
||||
|
||||
$scope.categoryButtonLabel = function (category) {
|
||||
var categoryLabel = $translate.instant('appstore.categoryLabel');
|
||||
|
||||
if (category === '') return $translate.instant('appstore.category.all');
|
||||
if (category === 'new') return $translate.instant('appstore.category.newApps');
|
||||
|
||||
var tmp = $scope.categories.find(function (c) { return c.id === category; });
|
||||
if (tmp) return tmp.label;
|
||||
|
||||
return categoryLabel;
|
||||
};
|
||||
|
||||
$scope.isProxyApp = function (app) {
|
||||
if (!app) return false;
|
||||
|
||||
return app.id === 'io.cloudron.builtin.appproxy';
|
||||
};
|
||||
|
||||
$scope.userManagementFilterOptions = [
|
||||
{ id: '', icon: '', label: $translate.instant('appstore.ssofilter.all') },
|
||||
{ id: 'sso', icon: 'fas fa-user', label: $translate.instant('apps.auth.sso') },
|
||||
{ id: 'nosso', icon: 'far fa-user', label: $translate.instant('apps.auth.nosso') },
|
||||
{ id: 'email', icon: 'fas fa-envelope', label: $translate.instant('apps.auth.email') },
|
||||
];
|
||||
$scope.userManagementFilterOption = $scope.userManagementFilterOptions[0];
|
||||
|
||||
$scope.userManagementFilterOptionIsActive = function (option) {
|
||||
return option.id === $scope.userManagementFilterOption.id;
|
||||
};
|
||||
|
||||
$scope.applyUserMangamentFilter = function (option) {
|
||||
$scope.userManagementFilterOption = option;
|
||||
};
|
||||
|
||||
$scope.appInstall = {
|
||||
busy: false,
|
||||
state: 'appInfo',
|
||||
error: {},
|
||||
app: {},
|
||||
needsOverwrite: false,
|
||||
subdomain: '',
|
||||
domain: null, // object and not the string
|
||||
secondaryDomains: {},
|
||||
ports: {},
|
||||
portsEnabled: {},
|
||||
mediaLinks: [],
|
||||
keyFile: null,
|
||||
keyFileName: '',
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
customAuth: false,
|
||||
optionalSso: false,
|
||||
subscriptionErrorMesssage: '',
|
||||
upstreamUri: '',
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
var tmp = $scope.appInstall.accessRestriction;
|
||||
return !!(tmp.users.length || tmp.groups.length);
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.needsOverwrite = false;
|
||||
$scope.appInstall.subdomain = '';
|
||||
$scope.appInstall.domain = null;
|
||||
$scope.appInstall.secondaryDomains = {};
|
||||
$scope.appInstall.ports = {};
|
||||
$scope.appInstall.state = 'appInfo';
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.accessRestrictionOption = '';
|
||||
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
||||
$scope.appInstall.optionalSso = false;
|
||||
$scope.appInstall.customAuth = false;
|
||||
$scope.appInstall.subscriptionErrorMesssage = '';
|
||||
$scope.appInstall.upstreamUri = '';
|
||||
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseSubscriptionRequired').collapse('hide');
|
||||
$('#collapseMediaLinksCarousel').collapse('show');
|
||||
|
||||
if ($scope.appInstallForm) {
|
||||
$scope.appInstallForm.$setPristine();
|
||||
$scope.appInstallForm.$setUntouched();
|
||||
}
|
||||
},
|
||||
|
||||
showForm: function (force) {
|
||||
var app = $scope.appInstall.app;
|
||||
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;
|
||||
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM
|
||||
var used = Client.getInstalledApps().reduce(function (prev, cur) {
|
||||
if (cur.runState === RSTATES.STOPPED) return prev;
|
||||
|
||||
return prev + (cur.memoryLimit || cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT);
|
||||
}, 0);
|
||||
var totalMemory = $scope.memory.memory * 2;
|
||||
var available = (totalMemory || 0) - used;
|
||||
|
||||
var enoughResourcesAvailable = (available - needed) >= 0;
|
||||
|
||||
if (enoughResourcesAvailable || force) {
|
||||
$scope.appInstall.state = 'installForm';
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('show');
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.state = 'resourceConstraint';
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('show');
|
||||
}
|
||||
},
|
||||
|
||||
show: function (app) { // this is an appstore app object!
|
||||
$scope.appInstall.reset();
|
||||
|
||||
// make a copy to work with in case the app object gets updated while polling
|
||||
angular.copy(app, $scope.appInstall.app);
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.domain = $scope.domains.find(function (d) { return $scope.config.adminDomain === d.domain; }); // pre-select the adminDomain
|
||||
|
||||
$scope.appInstall.secondaryDomains = {};
|
||||
var httpPorts = $scope.appInstall.app.manifest.httpPorts || {};
|
||||
for (var env2 in httpPorts) {
|
||||
$scope.appInstall.secondaryDomains[env2] = {
|
||||
subdomain: httpPorts[env2].defaultValue || '',
|
||||
domain: $scope.appInstall.domain
|
||||
};
|
||||
}
|
||||
|
||||
$scope.appInstall.portInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts); // Portbinding map only for information
|
||||
$scope.appInstall.ports = {}; // This holds the env:port pair
|
||||
$scope.appInstall.portsEnabled = {}; // This holds the enabled/disabled flag
|
||||
|
||||
var manifest = app.manifest;
|
||||
$scope.appInstall.optionalSso = !!manifest.optionalSso;
|
||||
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oidc'] || manifest.addons['proxyAuth']);
|
||||
|
||||
$scope.appInstall.accessRestrictionOption = $scope.groups.length ? '' : 'any'; // make the user select an ACL conciously if groups are used
|
||||
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
||||
|
||||
// set default ports
|
||||
var allPorts = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts);
|
||||
for (var env in allPorts) {
|
||||
$scope.appInstall.ports[env] = allPorts[env].defaultValue || 0;
|
||||
$scope.appInstall.portsEnabled[env] = true;
|
||||
}
|
||||
|
||||
$('#appInstallModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appInstall.busy = true;
|
||||
$scope.appInstall.error.other = null;
|
||||
$scope.appInstall.error.location = null;
|
||||
$scope.appInstall.error.port = null;
|
||||
|
||||
var secondaryDomains = {};
|
||||
for (var env2 in $scope.appInstall.secondaryDomains) {
|
||||
secondaryDomains[env2] = {
|
||||
subdomain: $scope.appInstall.secondaryDomains[env2].subdomain,
|
||||
domain: $scope.appInstall.secondaryDomains[env2].domain.domain
|
||||
};
|
||||
}
|
||||
|
||||
// only use enabled ports from ports
|
||||
var finalPorts = {};
|
||||
for (var env in $scope.appInstall.ports) {
|
||||
if ($scope.appInstall.portsEnabled[env]) {
|
||||
finalPorts[env] = $scope.appInstall.ports[env];
|
||||
}
|
||||
}
|
||||
|
||||
var finalAccessRestriction = null;
|
||||
if ($scope.appInstall.accessRestrictionOption === 'groups') {
|
||||
finalAccessRestriction = { users: [], groups: [] };
|
||||
finalAccessRestriction.users = $scope.appInstall.accessRestriction.users.map(function (u) { return u.id; });
|
||||
finalAccessRestriction.groups = $scope.appInstall.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
overwriteDns: $scope.appInstall.needsOverwrite,
|
||||
subdomain: $scope.appInstall.subdomain || '',
|
||||
domain: $scope.appInstall.domain.domain,
|
||||
secondaryDomains: secondaryDomains,
|
||||
ports: finalPorts,
|
||||
accessRestriction: finalAccessRestriction,
|
||||
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso'),
|
||||
};
|
||||
|
||||
if ($scope.appInstall.upstreamUri) {
|
||||
data.upstreamUri = $scope.appInstall.upstreamUri;
|
||||
data.upstreamUri = data.upstreamUri.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
var domains = [];
|
||||
domains.push({ subdomain: data.subdomain, domain: data.domain, type: 'primary' });
|
||||
var canInstall = true;
|
||||
|
||||
async.eachSeries(domains, function (domain, callback) {
|
||||
if (data.overwriteDns) return callback();
|
||||
|
||||
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var message;
|
||||
if (result.error) {
|
||||
if (result.error.reason === ERROR.ACCESS_DENIED) {
|
||||
message = 'DNS credentials for ' + domain.domain + ' are invalid. Update it in Domains & Certs view';
|
||||
if (domain.type === 'primary') {
|
||||
$scope.appInstall.error.location = message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
} else {
|
||||
if (domain.type === 'primary') {
|
||||
$scope.appInstall.error.location = result.error.message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
}
|
||||
canInstall = false;
|
||||
} else if (result.needsOverwrite) {
|
||||
message = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
|
||||
if (data.type === 'primary') {
|
||||
$scope.appInstall.error.location = message;
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = message;
|
||||
}
|
||||
$scope.appInstall.needsOverwrite = true;
|
||||
canInstall = false;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) {
|
||||
$scope.appInstall.busy = false;
|
||||
return Client.error(error);
|
||||
}
|
||||
|
||||
if (!canInstall) {
|
||||
$scope.appInstall.busy = false;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, data, function (error, newAppId) {
|
||||
if (error) {
|
||||
var errorMessage = error.message.toLowerCase();
|
||||
|
||||
if (error.statusCode === 402) {
|
||||
$scope.appInstall.state = 'subscriptionRequired';
|
||||
$scope.appInstall.subscriptionErrorMesssage = error.message;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseSubscriptionRequired').collapse('show');
|
||||
} else if (error.statusCode === 409) {
|
||||
if (errorMessage.indexOf('port') !== -1) {
|
||||
$scope.appInstall.error.port = error.message;
|
||||
} else if (errorMessage.indexOf('location') !== -1) {
|
||||
if (errorMessage.indexOf('primary') !== -1) {
|
||||
$scope.appInstall.error.location = error.message;
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.error.secondaryDomain = error.message;
|
||||
}
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
|
||||
// stash new app id for later
|
||||
$scope.appInstall.app.id = newAppId;
|
||||
|
||||
// we track the postinstall confirmation for the current user's browser
|
||||
// TODO later we might want to have a notification db to track the state across admins and browsers
|
||||
if ($scope.appInstall.app.manifest.postInstallMessage) {
|
||||
localStorage['confirmPostInstall_' + $scope.appInstall.app.id] = true;
|
||||
}
|
||||
|
||||
$scope.showView('/apps');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appNotFound = {
|
||||
appId: '',
|
||||
version: ''
|
||||
};
|
||||
|
||||
$scope.appstoreLogin = {
|
||||
busy: false,
|
||||
error: {},
|
||||
email: '',
|
||||
password: '',
|
||||
totpToken: '',
|
||||
setupType: 'login',
|
||||
termsAccepted: false,
|
||||
setupToken: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.appstoreLogin.error = {};
|
||||
$scope.appstoreLogin.busy = true;
|
||||
|
||||
var func = $scope.appstoreLogin.setupToken ? Client.registerCloudronWithSetupToken.bind(null, $scope.appstoreLogin.setupToken) : Client.registerCloudron.bind(null, $scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.setupType === 'register');
|
||||
func(function (error) {
|
||||
if (error) {
|
||||
$scope.appstoreLogin.busy = false;
|
||||
|
||||
if (error.statusCode === 409) {
|
||||
$scope.appstoreLogin.error.email = 'An account with this email already exists';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreSignupForm.email.$setPristine();
|
||||
$scope.appstoreSignupForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else if (error.statusCode === 412) {
|
||||
if (error.message.indexOf('TOTP token missing') !== -1) {
|
||||
$scope.appstoreLogin.error.totpToken = 'A 2FA token is required';
|
||||
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
||||
} else if (error.message.indexOf('TOTP token invalid') !== -1) {
|
||||
$scope.appstoreLogin.error.totpToken = 'Wrong 2FA token';
|
||||
$scope.appstoreLogin.totpToken = '';
|
||||
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
|
||||
} else {
|
||||
$scope.appstoreLogin.error.loginPassword = 'Wrong email or password';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$('#inputAppstoreLoginPassword').focus();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
}
|
||||
} else if (error.statusCode === 424) {
|
||||
if (error.message === 'wrong user') {
|
||||
$scope.appstoreLogin.error.generic = 'Wrong cloudron.io account';
|
||||
$scope.appstoreLogin.email = '';
|
||||
$scope.appstoreLogin.password = '';
|
||||
$scope.appstoreLoginForm.email.$setPristine();
|
||||
$scope.appstoreLoginForm.password.$setPristine();
|
||||
$scope.appstoreSignupForm.email.$setPristine();
|
||||
$scope.appstoreSignupForm.password.$setPristine();
|
||||
$('#inputAppstoreLoginEmail').focus();
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message;
|
||||
}
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appstoreLogin.error.setupToken = 'Invalid or expired setup token';
|
||||
$scope.appstoreLogin.setupToken = '';
|
||||
$scope.appstoreSetupTokenForm.setupToken.$setPristine();
|
||||
$('#inputAppstoreSetupToken').focus();
|
||||
} else {
|
||||
console.error(error);
|
||||
$scope.appstoreLogin.error.generic = error.message || 'Please retry later';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// do a full re-init of the view now that we have a subscription
|
||||
init();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// TODO does not support testing apps in search
|
||||
$scope.search = function () {
|
||||
if (!$scope.searchString) return $scope.showCategory($scope.cachedCategory);
|
||||
|
||||
$scope.category = '';
|
||||
|
||||
Client.getAppstoreAppsFast(function (error, apps) {
|
||||
if (error) return $timeout($scope.search, 1000);
|
||||
|
||||
var token = $scope.searchString.toUpperCase();
|
||||
|
||||
$scope.popularApps = [];
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
// on searches we give highe priority if title or tagline matches
|
||||
app.priority = 0;
|
||||
|
||||
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) {
|
||||
app.priority = 2;
|
||||
return true;
|
||||
}
|
||||
if (app.manifest.tagline.toUpperCase().indexOf(token) !== -1) {
|
||||
app.priority = 1;
|
||||
return true;
|
||||
}
|
||||
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function filterForNewApps(apps) {
|
||||
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
||||
var tmp = [];
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
var offset = moment().subtract(i++, 'days');
|
||||
tmp = apps.filter(function (app) { return moment(app.publishedAt).isAfter(offset); });
|
||||
} while(tmp.length < minApps);
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function filterForRecentlyUpdatedApps(apps) {
|
||||
var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
|
||||
var tmp = [];
|
||||
var i = 0;
|
||||
|
||||
do {
|
||||
var offset = moment().subtract(i++, 'days');
|
||||
tmp = apps.filter(function (app) { return moment(app.creationDate).isAfter(offset); }); // creationDate here is from appstore's appversions table
|
||||
} while(tmp.length < minApps);
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
$scope.showCategory = function (category) {
|
||||
$scope.category = category;
|
||||
|
||||
$scope.cachedCategory = $scope.category;
|
||||
|
||||
Client.getAppstoreAppsFast(function (error, apps) {
|
||||
if (error) return $timeout($scope.showCategory.bind(null, category), 1000);
|
||||
|
||||
if (!$scope.category) {
|
||||
$scope.apps = apps.slice(0).filter(function (app) { return !app.featured; }).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
|
||||
$scope.popularApps = apps.slice(0).filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
||||
} else if ($scope.category === 'new') {
|
||||
$scope.apps = filterForNewApps(apps);
|
||||
} else if ($scope.category === 'recent') {
|
||||
$scope.apps = filterForRecentlyUpdatedApps(apps);
|
||||
} else {
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
return app.manifest.tags.some(function (tag) { return $scope.category.toUpperCase() === tag.toUpperCase(); }); // reverse sort;
|
||||
}).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
|
||||
}
|
||||
|
||||
// ensure we scroll to top
|
||||
document.getElementById('ng-view').scrollTop = 0;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error) return console.error('Unable to get subscription.', error);
|
||||
|
||||
Client.openSubscriptionSetup(subscription);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showAppNotFound = function (appId, version) {
|
||||
$scope.appNotFound.appId = appId;
|
||||
$scope.appNotFound.version = version || 'latest';
|
||||
|
||||
$('#appNotFoundModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.gotoApp = function (app) {
|
||||
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
|
||||
};
|
||||
|
||||
$scope.openAppProxy = function () {
|
||||
$location.path('/appstore/io.cloudron.builtin.appproxy', false).search({});
|
||||
};
|
||||
|
||||
$scope.applinksAdd = {
|
||||
error: {},
|
||||
busy: false,
|
||||
upstreamUri: '',
|
||||
label: '',
|
||||
tags: '',
|
||||
accessRestrictionOption: 'any',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
return !!($scope.applinksAdd.accessRestriction.users.length || $scope.applinksAdd.accessRestriction.groups.length);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.applinksAdd.error = {};
|
||||
$scope.applinksAdd.busy = false;
|
||||
$scope.applinksAdd.upstreamUri = '';
|
||||
$scope.applinksAdd.label = '';
|
||||
$scope.applinksAdd.tags = '';
|
||||
|
||||
$scope.applinksAddForm.$setUntouched();
|
||||
$scope.applinksAddForm.$setPristine();
|
||||
|
||||
$('#applinksAddModal').modal('show');
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
if (!$scope.applinksAdd.upstreamUri) return;
|
||||
|
||||
$scope.applinksAdd.busy = true;
|
||||
$scope.applinksAdd.error = {};
|
||||
|
||||
var accessRestriction = null;
|
||||
if ($scope.applinksAdd.accessRestrictionOption === 'groups') {
|
||||
accessRestriction = { users: [], groups: [] };
|
||||
accessRestriction.users = $scope.applinksAdd.accessRestriction.users.map(function (u) { return u.id; });
|
||||
accessRestriction.groups = $scope.applinksAdd.accessRestriction.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
var data = {
|
||||
upstreamUri: $scope.applinksAdd.upstreamUri,
|
||||
label: $scope.applinksAdd.label,
|
||||
accessRestriction: accessRestriction,
|
||||
tags: $scope.applinksAdd.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; })
|
||||
};
|
||||
|
||||
Client.addApplink(data, function (error) {
|
||||
$scope.applinksAdd.busy = false;
|
||||
|
||||
if (error && error.statusCode === 400 && error.message.includes('upstreamUri')) {
|
||||
$scope.applinksAdd.error.upstreamUri = error.message;
|
||||
$scope.applinksAddForm.$setUntouched();
|
||||
$scope.applinksAddForm.$setPristine();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Failed to add applink', error);
|
||||
|
||||
$scope.showView('/apps');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function hashChangeListener() {
|
||||
// event listener is called from DOM not angular, need to use $apply
|
||||
$scope.$apply(function () {
|
||||
var appId = $location.path().slice('/appstore/'.length);
|
||||
var version = $location.search().version;
|
||||
|
||||
if (appId) {
|
||||
Client.getAppstoreAppByIdAndVersion(appId, version || 'latest', function (error, result) {
|
||||
if (error) {
|
||||
$scope.showAppNotFound(appId, version);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appInstall.show(result);
|
||||
});
|
||||
} else {
|
||||
$scope.appInstall.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchUsers() {
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchUsers, 5000);
|
||||
}
|
||||
|
||||
$scope.users = users;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchGroups() {
|
||||
Client.getGroups(function (error, groups) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchGroups, 5000);
|
||||
}
|
||||
|
||||
$scope.groups = groups;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMemory() {
|
||||
Client.memory(function (error, memory) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchMemory, 5000);
|
||||
}
|
||||
|
||||
$scope.memory = memory;
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription(callback) {
|
||||
var validSubscription = false;
|
||||
|
||||
Client.getSubscription(function (error, subscription) {
|
||||
if (error) {
|
||||
if (error.statusCode === 412) { // not registered yet
|
||||
validSubscription = false;
|
||||
} else if (error.statusCode === 402) { // invalid token, license error
|
||||
validSubscription = false;
|
||||
} else { // 424/external error?
|
||||
return callback(error);
|
||||
}
|
||||
} else {
|
||||
validSubscription = true;
|
||||
$scope.subscription = subscription;
|
||||
}
|
||||
|
||||
// clear busy state when a login/signup was performed
|
||||
$scope.appstoreLogin.busy = false;
|
||||
|
||||
// also update the root controller status
|
||||
if ($scope.$parent) $scope.$parent.updateSubscriptionStatus();
|
||||
|
||||
callback(null, validSubscription);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
Client.getAppstoreAppsFast(function (error) {
|
||||
if (error && error.statusCode === 402) {
|
||||
$scope.validSubscription = false;
|
||||
$scope.ready = true;
|
||||
return;
|
||||
} else if (error) {
|
||||
console.error('Failed to get apps. Will retry.', error);
|
||||
$timeout(init, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.showCategory('');
|
||||
|
||||
getSubscription(function (error, validSubscription) {
|
||||
if (error) console.error('Failed to get subscription.', error);
|
||||
|
||||
// autofocus login
|
||||
if (!validSubscription) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
|
||||
|
||||
$scope.validSubscription = validSubscription;
|
||||
$scope.ready = true;
|
||||
|
||||
// refresh everything in background
|
||||
Client.getAppstoreApps(function (error) { if (error) console.error('Failed to fetch apps.', error); });
|
||||
Client.refreshConfig(); // refresh domain, user, group limit etc
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
fetchMemory();
|
||||
|
||||
// domains is required since we populate the dropdown with domains[0]
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error('Error getting domains.', error);
|
||||
|
||||
$scope.domains = result;
|
||||
|
||||
// show install app dialog immediately if an app id was passed in the query
|
||||
// hashChangeListener calls $apply, so make sure we don't double digest here
|
||||
setTimeout(hashChangeListener, 1);
|
||||
|
||||
setTimeout(function () { $('#appstoreSearch').focus(); }, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(init);
|
||||
|
||||
$('#appInstallModal').on('hidden.bs.modal', function () {
|
||||
// clear the appid and version in the search bar when dialog is cancelled
|
||||
$scope.$apply(function () {
|
||||
$location.path('/appstore', false).search({ }); // 'false' means do not reload
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', hashChangeListener);
|
||||
|
||||
$scope.$on('$destroy', function handler() {
|
||||
window.removeEventListener('hashchange', hashChangeListener);
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['appInstallModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,816 +0,0 @@
|
||||
<!-- Modal details -->
|
||||
<div class="modal fade" id="backupDetailsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" style="width: 750px">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupDetails.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.id' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.id }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.label' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.label }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupEdit.remotePath' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.remotePath }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.date' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.creationTime | prettyLongDate }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.version' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">v{{ backupDetails.backup.packageVersion }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-2 text-muted">{{ 'backups.backupDetails.format' | tr }}:</div>
|
||||
<div class="col-xs-10 text-right">{{ backupDetails.backup.format }}</div>
|
||||
</div>
|
||||
<br/>
|
||||
<p class="text-muted">{{ 'backups.backupDetails.list' | tr:{ appCount: backupDetails.backup.contents.length } }}:</p>
|
||||
<span ng-repeat="content in backupDetails.backup.contents | orderBy:['label','fqdn']">
|
||||
<a ng-if="content.fqdn" ng-href="/#/app/{{content.id}}/backups">{{ content.label || content.fqdn }}</a>
|
||||
<a ng-if="!content.fqdn" ng-href="/#/eventlog?search={{content.id}}">{{ content.id }}</a>
|
||||
<span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit individual backup (label and retention sec) -->
|
||||
<div class="modal fade" id="editBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupEdit.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="editBackupForm" role="form" novalidate ng-submit="editBackup.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="editBackup.error">{{ editBackup.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputBackupLabel">{{ 'backups.backupEdit.label' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="editBackup.label" id="inputBackupLabel" name="label" ng-disabled="editBackup.busy" placeholder="" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="editBackup.persist">{{ 'backups.backupEdit.preserved.description' | tr }}</input>
|
||||
|
||||
<sup><a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{ 'backups.backupEdit.preserved.tooltip' | tr: { appsLength: editBackup.backup.contents.length} }}"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="editBackup.submit()" ng-disabled="editBackupForm.$invalid || editBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="editBackup.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal backup failed -->
|
||||
<div class="modal fade" id="createBackupFailedModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.backupFailed.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ createBackup.errorMessage }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup backups info -->
|
||||
<div class="modal fade" id="cleanupBackupsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.cleanupBackups.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">{{ 'backups.cleanupBackups.description' | tr }}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="cleanupBackups.start()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup schedule config -->
|
||||
<div class="modal fade" id="backupPolicyModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupSchedule.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="backupPolicyForm" role="form" novalidate ng-submit="backupPolicy.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="backupPolicy.error">{{ backupPolicy.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupSchedule">{{ 'backups.configureBackupSchedule.schedule' | tr }}</label>
|
||||
<p ng-bind-html="'backups.configureBackupSchedule.scheduleDescription' | tr"></p>
|
||||
|
||||
<div class="row" style="margin-left: 20px;">
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.days.length }">
|
||||
{{ 'backups.configureBackupSchedule.days' | tr }}: <multiselect id="backupSchedule" class="input-sm stretch" ng-model="backupPolicy.days" options="a.name for a in cronDays" data-multiple="true" ng-required></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.hours.length }">
|
||||
{{ 'backups.configureBackupSchedule.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="backupPolicy.hours" options="a.name for a in cronHours" data-multiple="true"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="backupRetention">{{ 'backups.configureBackupSchedule.retentionPolicy' | tr }}</label>
|
||||
<select class="form-control" id="backupRetention" ng-model="backupPolicy.retention" ng-options="a.value as a.name for a in backupRetentions"></select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="backupPolicy.submit()" ng-disabled="!backupPolicy.valid() || backupPolicy.busy"><i class="fa fa-circle-notch fa-spin" ng-show="backupPolicy.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal backup config -->
|
||||
<div class="modal fade" id="configureBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.configureBackupStorage.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="configureBackupForm" role="form" novalidate ng-submit="configureBackup.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="configureBackup.error">{{ configureBackup.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProviderProvider">{{ 'backups.configureBackupStorage.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small text-info" ng-show="backupConfig.provider !== configureBackup.provider">Backups in the old storage location have to be removed manually.</p>
|
||||
<select class="form-control" id="storageProviderProvider" ng-model="configureBackup.provider" ng-options="a.value as a.name for a in storageProviders" ng-change=configureBackup.clearProviderFields()></select>
|
||||
</div>
|
||||
|
||||
<!-- Noop -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'noop'">
|
||||
<p class="has-error">{{ 'backups.configureBackupStorage.noopNote' | tr }}</p>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.mountPoint || (configureBackupForm.mountPoint.$dirty && !configureBackup.mountPoint) }" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label class="control-label" for="inputConfigureMountPoint">{{ 'backups.configureBackupStorage.mountPoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountPoint" id="inputConfigureMountPoint" name="mountPoint" ng-disabled="configureBackup.busy" placeholder="/mnt/backups" ng-required="configureBackup.provider === 'mountpoint'">
|
||||
<p ng-show="configureBackup.provider === 'mountpoint'" ng-bind-html="'backups.configureBackupStorage.mountPointDescription' | tr:{ providerDocsLink: 'https://docs.cloudron.io/backups/#'+configureBackup.provider }"></p>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupHost">{{ 'backups.configureBackupStorage.server' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.host" id="configureBackupHost" name="host" ng-disabled="configureBackup.busy" placeholder="Server IP or hostname" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.mountOptions.seal">{{ 'backups.configureBackupStorage.cifsSealSupport' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/NFS/SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupRemoteDir">{{ 'backups.configureBackupStorage.remoteDirectory' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.remoteDir" id="configureBackupRemoteDir" name="remoteDir" ng-disabled="configureBackup.busy" placeholder="/share" ng-required="configureBackup.provider === 'cifs' || configureBackup.provider === 'nfs' || configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupUsername">{{ 'backups.configureBackupStorage.username' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.username" id="configureBackupUsername" name="cifsUsername" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<!-- CIFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'cifs'">
|
||||
<label class="control-label" for="configureBackupPassword">{{ 'backups.configureBackupStorage.password' | tr }} ({{ configureBackup.provider }})</label>
|
||||
<input type="password" class="form-control" ng-model="configureBackup.mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="configureBackup.busy" password-reveal>
|
||||
</div>
|
||||
|
||||
<!-- EXT4/XFS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
|
||||
<label class="control-label" for="inputConfigureDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'disk'">
|
||||
<label class="control-label">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
|
||||
<select class="form-control" ng-model="configureBackup.disk" ng-options="item as item.label for item in configureBackup.blockDevices track by item.path" ng-required="configureBackup.provider === 'disk'"></select>
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPort">{{ 'backups.configureBackupStorage.port' | tr }}</label>
|
||||
<input type="number" class="form-control" ng-model="configureBackup.mountOptions.port" id="configureBackupPort" name="port" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupUser">{{ 'backups.configureBackupStorage.user' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.user" id="configureBackupUser" name="user" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'">
|
||||
</div>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<div class="form-group" ng-show="configureBackup.provider === 'sshfs'">
|
||||
<label class="control-label" for="configureBackupPrivateKey">{{ 'backups.configureBackupStorage.privateKey' | tr }}</label>
|
||||
<textarea class="form-control" ng-model="configureBackup.mountOptions.privateKey" id="configureBackupPrivateKey" name="privateKey" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'sshfs'"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.backupFolder }" ng-show="configureBackup.provider === 'filesystem'">
|
||||
<label class="control-label" for="inputConfigureBackupFolder">{{ 'backups.configureBackupStorage.localDirectory' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.backupFolder" id="inputConfigureBackupFolder" name="backupFolder" ng-disabled="configureBackup.busy" placeholder="Directory for backups" ng-required="configureBackup.provider === 'filesystem'">
|
||||
</div>
|
||||
|
||||
<!-- Filesystem/SSHFS/CIFS/NFS/EXT4/mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'filesystem' || mountlike(configureBackup.provider)">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.useHardlinks">{{ 'backups.configureBackupStorage.hardlinksLabel' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'mountpoint' || configureBackup.provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.preserveAttributes">{{ 'backups.configureBackupStorage.preserveAttributesLabel' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.chown">{{ 'backups.configureBackupStorage.chown' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/UpCloud/B2/R2 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.endpoint }" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 'cloudflare-r2' || configureBackup.provider === 's3-v4-compat' || configureBackup.provider === 'idrive-e2'">
|
||||
<label class="control-label" for="inputConfigureBackupEndpoint">{{ 'backups.configureBackupStorage.s3Endpoint' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.endpoint" id="inputConfigureBackupEndpoint" name="endpoint" ng-disabled="configureBackup.busy" placeholder="URL" ng-required="configureBackup.provider === 'minio' || configureBackup.provider === 'upcloud-objectstorage' || configureBackup.provider === 'backblaze-b2' || configureBackup.provider === 'cloudflare-r2' || configureBackup.provider === 's3-v4-compat' || configureBackup.provider === 'idrive-e2'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'minio' || configureBackup.provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.acceptSelfSignedCerts">{{ 'backups.configureBackupStorage.acceptSelfSignedCerts' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.bucket }" ng-show="s3like(configureBackup.provider) || configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="inputConfigureBackupBucket">{{ 'backups.configureBackupStorage.bucketName' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.bucket" id="inputConfigureBackupBucket" name="bucket" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS/SSHFS/CIFS/NFS/B2 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.prefix }" ng-show="configureBackup.provider !== 'filesystem' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPrefix">{{ 'backups.configureBackupStorage.prefix' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="configureBackup.busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3'">
|
||||
<label class="control-label" for="inputConfigureBackupS3Region">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="configureBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="configureBackup.region" ng-disabled="configureBackup.busy" placeholder="Leave empty to use us-east-1 as default"></input>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputConfigureBackupDORegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'hetzner-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupHetznerRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupHetznerRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in hetznerRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'hetzner-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputConfigureBackupExoscaleRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'wasabi'">
|
||||
<label class="control-label" for="inputConfigureBackupWasabiRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupScalewayRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'linode-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupLinodeRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'linode-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ovh-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupOvhRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ovh-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'ionos-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupIonosRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupIonosRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in ionosRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'ionos-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'vultr-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupVultrRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupVultrRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in vultrRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'vultr-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 'contabo-objectstorage'">
|
||||
<label class="control-label" for="inputConfigureBackupContaboRegion">{{ 'backups.configureBackupStorage.region' | tr }}</label>
|
||||
<select class="form-control" name="region" id="inputConfigureBackupContaboRegion" ng-model="configureBackup.endpoint" ng-options="a.value as a.name for a in contaboRegions" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'contabo-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.accessKeyId }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupAccessKeyId">{{ 'backups.configureBackupStorage.s3AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.accessKeyId" id="inputConfigureBackupAccessKeyId" name="accessKeyId" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.secretAccessKey }" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="inputConfigureBackupSecretAccessKey">{{ 'backups.configureBackupStorage.s3SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="configureBackup.secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.gcsKeyInput }" ng-show="configureBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">{{ 'backups.configureBackupStorage.gcsServiceKey' | tr }}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="configureBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="storageFormat">{{ 'backups.configureBackupStorage.format' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small text-info" ng-show="backupConfig.format !== configureBackup.format">{{ 'backups.configureBackupStorage.formatChangeNote' | tr }}</p>
|
||||
<p class="small text-info" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">{{ 'backups.configureBackupStorage.s3LikeNote' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#amazon-s3" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<select class="form-control" id="storageFormat" ng-model="configureBackup.format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.password }" ng-show="configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="inputConfigureBackupPassword">{{ 'backups.configureBackupStorage.encryptionPassword' | tr }} <sup><a ng-href="https://docs.cloudron.io/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.encryptionDescription' | tr }}</p>
|
||||
<input type="text" class="form-control" name="encryptionPassword" ng-model="configureBackup.password" id="inputConfigureBackupPassword" ng-disabled="configureBackup.busy" placeholder="{{ 'backups.configureBackupStorage.encryptionPasswordPlaceholder' | tr }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.password && configureBackup.password !== SECRET_PLACEHOLDER" ng-class="{ 'has-error': (configureBackupForm.encryptionPassword.$dirty && configureBackup.password !== configureBackup.passwordRepeat) }">
|
||||
<label class="control-label" for="inputConfigureBackupPasswordRepeat">{{ 'backups.configureBackupStorage.encryptionPasswordRepeat' | tr }}</label>
|
||||
<input id="inputConfigureBackupPasswordRepeat" type="text" class="form-control" name="passwordRepeat" ng-model="configureBackup.passwordRepeat" ng-disabled="configureBackup.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="configureBackup.password !== '' && configureBackup.format === 'rsync'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.encryptedFilenames">{{ 'backups.configureBackupStorage.encryptFilenames' | tr }}</input>
|
||||
<sup><a ng-href="https://docs.cloudron.io/backups/#filenames" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a href="" ng-click="configureBackup.advancedVisible = true" ng-hide="configureBackup.advancedVisible">{{ 'backups.configureBackupStorage.advancedSettings' | tr }}</a>
|
||||
<div uib-collapse="!configureBackup.advancedVisible">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="sliderConfigureBackupMemoryLimit">{{ 'backups.configureBackupStorage.memoryLimit' | tr }}: <b>{{ configureBackup.memoryLimit | prettyBinarySize:'1024 MB' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.memoryLimitDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupMemoryLimit" ng-model="configureBackup.memoryLimit" step="{{ 256*1024*1024 }}" min="{{ MIN_MEMORY_LIMIT }}" max="{{ MAX_MEMORY_LIMIT }}" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="s3like(configureBackup.provider)">
|
||||
<label class="control-label" for="sliderConfigureBackupUploadPartSize">{{ 'backups.configureBackupStorage.uploadPartSize' | tr }}: <b>{{ configureBackup.uploadPartSize | prettyBinarySize:'Default (50 MiB)' }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadPartSizeDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupUploadPartSize" ng-model="configureBackup.uploadPartSize" list="uploadPartSizeTicks" step="{{ 1024*1024 }}" min="{{ 1024*1024 }}" max="{{ 1024*1024*1024 }}" />
|
||||
<datalist id="uploadPartSizeTicks">
|
||||
<option value="{{ 1024*1024 }}"></option>
|
||||
<option value="{{ 64*1024*1024 }}"></option>
|
||||
<option value="{{ 128*1024*1024 }}"></option>
|
||||
<option value="{{ 256*1024*1024 }}"></option>
|
||||
<option value="{{ 512*1024*1024 }}"></option>
|
||||
<option value="{{ 1024*1024*1024 }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && configureBackup.provider !== 'noop'">
|
||||
<label class="control-label" for="sliderConfigureBackupSyncConcurrency">{{ 'backups.configureBackupStorage.uploadConcurrency' | tr }}: <b>{{ configureBackup.syncConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.uploadConcurrencyDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupSyncConcurrency" ng-model="configureBackup.syncConcurrency" step="10" min="10" max="200" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label" for="sliderConfigureBackupDownloadConcurrency">{{ 'backups.configureBackupStorage.downloadConcurrency' | tr }}: <b>{{ configureBackup.downloadConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.downloadConcurrencyDescription' | tr }}</p>
|
||||
<input type="range" id="sliderConfigureBackupDownloadConcurrency" ng-model="configureBackup.downloadConcurrency" step="10" min="10" max="200" />
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="configureBackup.format === 'rsync' && (s3like(configureBackup.provider) || configureBackup.provider === 'gcs')">
|
||||
<label class="control-label" for="sliderConfigureBackupCopyConcurrency">{{ 'backups.configureBackupStorage.copyConcurrency' | tr }}: <b>{{ configureBackup.copyConcurrency }}</b></label>
|
||||
<p class="small">{{ 'backups.configureBackupStorage.copyConcurrencyDescription' | tr }}
|
||||
<span ng-show="configureBackup.provider === 'digitalocean-spaces'">{{ 'backups.configureBackupStorage.copyConcurrencyDigitalOceanNote' | tr }}</span>
|
||||
</p>
|
||||
<input type="range" id="sliderConfigureBackupCopyConcurrency" ng-model="configureBackup.copyConcurrency" step="10" min="10" max="500" />
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureBackup.submit()" ng-disabled="configureBackupForm.$invalid || configureBackup.busy || (configureBackup.password !== SECRET_PLACEHOLDER && configureBackup.password !== configureBackup.passwordRepeat)"><i class="fa fa-circle-notch fa-spin" ng-show="configureBackup.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal archive restore -->
|
||||
<div class="modal fade" id="restoreArchiveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.restoreArchiveDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 0 15px">
|
||||
<p ng-bind-html="'backups.restoreArchiveDialog.description' | tr:{ appId: archiveRestore.manifest.id, fqdn: archiveRestore.fqdn, creationTime: (archiveRestore.archive.creationTime | prettyLongDate) }"></p>
|
||||
<form role="form" ng-submit="archiveRestore.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': archiveRestore.error.location.fqdn === archiveRestore.subdomain + '.' + archiveRestore.domain.domain }">
|
||||
<label class="control-label" for="cloneLocationInput">{{ 'app.cloneDialog.location' | tr }}</label>
|
||||
<div ng-show="archiveRestore.error.location.fqdn === archiveRestore.subdomain + '.' + archiveRestore.domain.domain"><small>{{ archiveRestore.error.location.message }}</small></div>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="archiveRestore.subdomain" id="cloneLocationInput" name="location" placeholder="{{ 'appstore.installDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ '.' + archiveRestore.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="archiveRestore.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="has-error text-center" ng-show="archiveRestore.error.secondaryDomain">{{ archiveRestore.error.secondaryDomain }}</div>
|
||||
<div ng-repeat="(env, info) in archiveRestore.manifest.httpPorts">
|
||||
<ng-form name="secondaryDomainInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!secondaryDomainInfo_form.itemName{{$index}}.$dirty && archiveRestore.error.secondaryDomain) || (secondaryDomainInfo_form.itemName{{$index}}.$dirty && secondaryDomainInfo_form.itemName{{$index}}.$invalid) || (archiveRestore.error.location.fqdn === archiveRestore.secondaryDomains[env].subdomain + '.' + archiveRestore.secondaryDomains[env].domain.domain) }">
|
||||
<label class="control-label" for="secondaryDomainInput{{env}}">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
</label>
|
||||
|
||||
<div ng-show="archiveRestore.error.location.fqdn === archiveRestore.secondaryDomains[env].subdomain + '.' + archiveRestore.secondaryDomains[env].domain.domain"><small>{{ archiveRestore.error.location.message }}</small></div>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="archiveRestore.secondaryDomains[env].subdomain" name="location{{$index}}" placeholder="{{ 'app.location.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>.{{ archiveRestore.secondaryDomains[env].domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="archiveRestore.secondaryDomains[env].domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<p class="text-small text-warning" ng-show="archiveRestore.domain.provider === 'noop' || archiveRestore.domain.provider === 'manual'" ng-bind-html="'appstore.installDialog.manualWarning' | tr:{ location: ((archiveRestore.subdomain ? archiveRestore.subdomain + '.' : '') + archiveRestore.domain.domain) }"></p>
|
||||
|
||||
<div class="has-error text-center" ng-show="archiveRestore.error.port">{{ archiveRestore.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in archiveRestore.portInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!archiveRestore.itemName{{$index}}.$dirty && archiveRestore.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="archiveRestore.portsEnabled[env]">
|
||||
{{ info.title }}
|
||||
<sup>
|
||||
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}}"><i class="fa fa-question-circle"></i></a>
|
||||
</sup>
|
||||
<small style="padding-left: 5px;" ng-show="info.readOnly">{{ 'appstore.installDialog.portReadOnly' | tr }}</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" ng-model="archiveRestore.ports[env]" ng-disabled="!archiveRestore.portsEnabled[env]" ng-readonly="info.readOnly" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
<p class="text-small text-warning text-bold" ng-show="archiveRestore.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="archiveRestore.submit()" ng-disabled="archiveRestore.busy"><i class="fas fa-history" ng-hide="archiveRestore.busy"></i><i class="fa fa-circle-notch fa-spin" ng-show="archiveRestore.busy"></i> {{ 'backups.restoreArchiveDialog.restoreAction' | tr:{ dnsOverwrite: archiveRestore.needsOverwrite } }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal delete archive -->
|
||||
<div class="modal fade" id="archiveDeleteModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'backups.deleteArchiveDialog.title' | tr:{ appTitle: archiveDelete.title, fqdn: archiveDelete.fqdn } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'backups.deleteArchiveDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="archiveDelete.submit()" ng-disabled="archiveDelete.busy"><i class="fa fa-circle-notch fa-spin" ng-show="archiveDelete.busy"></i> {{ 'backups.deleteArchive.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<h1 class="section-header">{{ 'backups.title' | tr }}</h1>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'backups.location.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p>{{ 'backups.location.description' | tr }}
|
||||
<span ng-show="manualBackupApps.length">
|
||||
{{ 'backups.location.disabledList' | tr }}
|
||||
<span ng-repeat="app in manualBackupApps">
|
||||
<a ng-href="/#/app/{{app.id}}/backups">{{app.label || app.fqdn}}</a><span ng-hide="$last">,</span>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p ng-show="backupConfig.provider === 'noop'" class="text-danger" ng-bind-html="'backups.check.noop' | tr | markdown2html"></p>
|
||||
<p ng-show="backupConfig.provider === 'filesystem'" class="text-danger" ng-bind-html="'backups.check.sameDisk' | tr | markdown2html"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.provider' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ prettyProviderName(backupConfig.provider) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="backupConfig.provider !== 'noop'">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.location' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right no-wrap">
|
||||
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
|
||||
<span ng-show="mountlike(backupConfig.provider)">
|
||||
<i class="fa fa-circle" ng-style="{ color: mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="mountStatus" uib-tooltip="{{ mountStatus.message }}"></i>
|
||||
<span ng-show="backupConfig.provider === 'disk' || backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</span>
|
||||
|
||||
<span ng-show="backupConfig.provider !== 's3' && backupConfig.provider !== 'minio' && (s3like(backupConfig.provider) || backupConfig.provider === 'gcs')">{{ backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 's3'">{{ backupConfig.region + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
<span ng-show="backupConfig.provider === 'minio'">{{ backupConfig.endpoint + ' ' + backupConfig.bucket + (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backupConfig.endpoint && backupConfig.provider !== 'minio'">
|
||||
<div class="col-xs-3">
|
||||
<span class="text-muted">{{ 'backups.location.endpoint' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<span>{{ backupConfig.endpoint || backupConfig.region }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'backups.location.format' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ backupConfig.format }} <i class="fas fa-lock" ng-show="backupConfig.password" ></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureBackup.show()">{{ 'backups.location.configure' | tr }}</button>
|
||||
<button class="btn btn-outline btn-default pull-right" ng-show="user.isAtLeastOwner && mountlike(backupConfig.provider)" ng-disabled="remount.busy" ng-click="remount.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="remount.busy"></i> {{ 'backups.location.remount' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">
|
||||
{{ 'backups.schedule.title' | tr }}
|
||||
<!-- <a class="btn btn-sm btn-default pull-right" ng-href="/logs.html?taskId={{cleanupBackups.taskId}}" target="_blank" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}"><i class="fas fa-align-left"></i></a> -->
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="cleanupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in cleanupTasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<p ng-bind-html=" 'backups.schedule.description' | tr "></p>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'backups.schedule.schedule' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">
|
||||
<span>{{ prettyBackupSchedule(backupPolicy.currentPolicy.schedule) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<span class="text-muted">{{ 'backups.schedule.retentionPolicy' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-8 text-right">
|
||||
<span>{{ prettyBackupRetention(backupPolicy.currentPolicy.retention) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-default" ng-click="cleanupBackups.ask()" ng-disabled="cleanupBackups.busy" style="margin-right: 5px"><i class="fa fa-circle-notch fa-spin" ng-show="cleanupBackups.busy"></i> {{ 'backups.listing.cleanupBackups' | tr }}</button>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="backupPolicy.show()">{{ 'backups.schedule.configure' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">
|
||||
{{ 'backups.listing.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="backupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in backupTasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!backups.length">{{ 'backups.listing.noBackups' | tr }}</p>
|
||||
|
||||
<table class="table table-hover" style="margin: 0;" ng-hide="!backups.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20px"></th>
|
||||
<th>{{ 'backups.listing.version' | tr }}</th>
|
||||
<th>{{ 'main.table.date' | tr }}</th>
|
||||
<th class="hide-mobile">{{ 'backups.listing.contents' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="backup in backups">
|
||||
<td><i class="fas fa-archive" ng-show="backup.preserveSecs === -1" uib-tooltip="{{ 'backups.listing.tooltipPreservedBackup' | tr }}"></i></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">v{{ backup.packageVersion }}</td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand">{{ backup.creationTime | prettyLongDate }} <b ng-show="backup.label">({{ backup.label }})</b></td>
|
||||
<td ng-click="backupDetails.show(backup)" class="hand hide-mobile">
|
||||
<span ng-show="!backup.contents.length">{{ 'backups.listing.noApps' | tr }}</span>
|
||||
<span ng-show="backup.contents.length">{{ 'backups.listing.appCount' | tr:{ appCount: backup.contents.length } }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="editBackup.show(backup)" uib-tooltip="{{ 'backups.listing.tooltipEditBackup' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="downloadConfig(backup)" uib-tooltip="{{ 'backups.listing.tooltipDownloadBackupConfig' | tr }}"><i class="fas fa-file-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row" ng-show="createBackup.busy">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%"></div>
|
||||
</div>
|
||||
<p>{{ createBackup.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="!createBackup.busy && !createBackup.active && createBackup.errorMessage">
|
||||
<div class="col-md-12">
|
||||
<p class="has-error">{{ createBackup.errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy">{{ 'backups.listing.backupNow' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopTask()" ng-show="createBackup.busy">{{ 'backups.listing.stopTask' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">
|
||||
{{ 'backups.archives.title' | tr }}
|
||||
</h3>
|
||||
|
||||
<div class="card card-large">
|
||||
<p ng-bind-html=" 'backups.archive.description' | tr "></p>
|
||||
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!archiveList.ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="archiveList.ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"></th> <!-- icon -->
|
||||
<th style="width: 35%">{{ 'backups.archives.location' | tr }}</th>
|
||||
<th style="width: 35%" class="hide-mobile">{{ 'backups.archives.info' | tr }}</th>
|
||||
<th style="width: 20%">{{ 'main.table.date' | tr }}</th>
|
||||
<th style="width: 5%" class="text-right hide-mobile">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="archive in archiveList.archives">
|
||||
<td>
|
||||
<img ng-src="{{ archive.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" height="48" width="48"/>
|
||||
</td>
|
||||
<!-- for pre-8.2 backups, appConfig can be null -->
|
||||
<td class="hand elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;" ng-click="archiveRestore.show(archive)">
|
||||
{{ archive.appConfig ? archive.appConfig.fqdn : '-' }}
|
||||
</td>
|
||||
<td class="hand elide-table-cell hide-mobile" style="text-overflow: ellipsis; white-space: nowrap;" ng-click="archiveRestore.show(archive)">
|
||||
<span uib-tooltip="{{ archive.manifest.id }}@{{ archive.manifest.version }}">{{ archive.manifest.title }}</span>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" style="text-overflow: ellipsis; white-space: nowrap;" ng-click="archiveRestore.show(archive)">
|
||||
{{ archive.creationTime | prettyDate }}
|
||||
</td>
|
||||
<td class="text-right no-wrap hide-mobile" style="vertical-align: middle;">
|
||||
<button class="btn btn-xs btn-default" ng-click="archiveRestore.show(archive)" uib-tooltip="Restore from Archive"><i class="fas fa-history"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="downloadConfig(archive, true)" uib-tooltip="{{ 'backups.listing.tooltipDownloadBackupConfig' | tr }}"><i class="fas fa-file-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="archiveDelete.ask(archive)" uib-tooltip="Delete Archive"><i class="fa fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,98 +0,0 @@
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'branding.changeLogo.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body branding-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{ avatarChange.avatarUrl() }}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="avatarChange.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="avatarChange.showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="avatarChange.setAvatar()"> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<h1 class="section-header">{{ 'branding.title' | tr }}</h1>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="aboutForm" ng-submit="about.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': about.error.cloudronName }">
|
||||
<label class="control-label">{{ 'branding.cloudronName' | tr }}</label>
|
||||
<div class="control-label" ng-show="about.error.cloudronName">{{about.error.cloudronName}}</div>
|
||||
<input type="text" class="form-control" id="inputCloudronName" name="name" ng-model="about.cloudronName" ng-minlength="1" maxlength="64" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'branding.logo' | tr }}</label>
|
||||
</div>
|
||||
<div class="branding-avatar" ng-click="avatarChange.showChangeAvatar()">
|
||||
<img ng-src="{{ about.avatarUrl() }}"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label class="control-label">{{ 'branding.backgroundImage' | tr }}</label>
|
||||
<div class="branding-background" ng-click="background.selectNew()">
|
||||
<img ng-src="{{ background.url() }}" onerror="this.src = '/img/background-image-placeholder.svg'"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<a href="" ng-show="!background.cleared" ng-click="background.clear()">{{ 'branding.clearBackgroundImage' | tr }}</a>
|
||||
<input type="file" id="backgroundFileInput" style="display: none" accept="image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="(!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="about.submit()" ng-disabled="false && (!about.avatar && !aboutForm.$dirty) || aboutForm.$invalid || about.busy"><i class="fa fa-circle-notch fa-spin" ng-show="about.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">{{ 'branding.footer.title' | tr }}</h3>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="footerForm" autocomplete="off">
|
||||
<p>{{ 'branding.footer.description' | tr }} <sup><a ng-href="https://docs.cloudron.io/branding/#footer" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<textarea name="footer" class="form-control" ng-model="footer.content" ng-disabled="footer.busy"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="footer.submit()" ng-disabled="!footerForm.$dirty || footerForm.$invalid || footer.busy"><i class="fa fa-circle-notch fa-spin" ng-show="footer.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,319 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular:false */
|
||||
/* global $:false */
|
||||
|
||||
angular.module('Application').controller('BrandingController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
$scope.avatarChange = {
|
||||
avatar: null, // { file, data, url }
|
||||
|
||||
availableAvatars: [{
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo.png',
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-green.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-orange.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-darkblue.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-red.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-yellow.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/logo-black.png'
|
||||
}],
|
||||
|
||||
avatarUrl: function () {
|
||||
if ($scope.avatarChange.avatar) {
|
||||
return $scope.avatarChange.avatar.data || $scope.avatarChange.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
getBlobFromImg: function (img, callback) {
|
||||
var size = 512;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
var imageDimensionRatio = img.width / img.height;
|
||||
var canvasDimensionRatio = canvas.width / canvas.height;
|
||||
var renderableHeight, renderableWidth, xStart, yStart;
|
||||
|
||||
if (imageDimensionRatio > canvasDimensionRatio) {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = img.width * (renderableHeight / img.height);
|
||||
xStart = (canvas.width - renderableWidth) / 2;
|
||||
yStart = 0;
|
||||
} else if (imageDimensionRatio < canvasDimensionRatio) {
|
||||
renderableWidth = canvas.width;
|
||||
renderableHeight = img.height * (renderableWidth / img.width);
|
||||
xStart = 0;
|
||||
yStart = (canvas.height - renderableHeight) / 2;
|
||||
} else {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = canvas.width;
|
||||
xStart = 0;
|
||||
yStart = 0;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, xStart, yStart, renderableWidth, renderableHeight);
|
||||
|
||||
canvas.toBlob(callback);
|
||||
},
|
||||
|
||||
setPreviewAvatar: function (avatar) {
|
||||
$scope.avatarChange.avatar = avatar;
|
||||
},
|
||||
|
||||
showChangeAvatar: function () {
|
||||
$scope.avatarChange.avatar = $scope.about.avatar;
|
||||
$('#avatarChangeModal').modal('show');
|
||||
},
|
||||
|
||||
showCustomAvatarSelector: function () {
|
||||
$('#avatarFileInput').click();
|
||||
},
|
||||
|
||||
setAvatar: function () {
|
||||
if (angular.equals($scope.about.avatar, $scope.avatarChange.avatar)) return $('#avatarChangeModal').modal('hide'); // nothing changed
|
||||
|
||||
$scope.about.avatar = $scope.avatarChange.avatar;
|
||||
|
||||
// get the blob now, we cannot get it if dialog is hidden
|
||||
var img = document.getElementById('previewAvatar');
|
||||
$scope.avatarChange.getBlobFromImg(img, function (blob) {
|
||||
$scope.about.avatarBlob = blob;
|
||||
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$('#avatarFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
var tmp = {
|
||||
file: event.target.files[0],
|
||||
data: fr.result,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.avatarChange.availableAvatars.push(tmp);
|
||||
$scope.avatarChange.setPreviewAvatar(tmp);
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.background = {
|
||||
enabled: false,
|
||||
file: null,
|
||||
src: null,
|
||||
cleared: false,
|
||||
newImageFile: null,
|
||||
cacheBusting: Date.now(),
|
||||
|
||||
url() {
|
||||
if ($scope.background.cleared) return '/img/background-image-placeholder.svg';
|
||||
else if ($scope.background.src) return $scope.background.src;
|
||||
else return `${Client.apiOrigin}/api/v1/cloudron/background?${$scope.background.cacheBusting}`;
|
||||
},
|
||||
|
||||
selectNew() {
|
||||
document.getElementById('backgroundFileInput').click();
|
||||
},
|
||||
|
||||
submit(callback) {
|
||||
if ($scope.background.cleared) {
|
||||
Client.changeCloudronBackground(null, callback);
|
||||
} else if ($scope.background.newImageFile) {
|
||||
Client.changeCloudronBackground($scope.background.newImageFile, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
$scope.background.cleared = true;
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('backgroundFileInput').onchange = function (event) {
|
||||
const fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
const image = new Image();
|
||||
image.onload = function () {
|
||||
// convert and scale to webp max 4k
|
||||
const maxWidth = 4096;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
if (image.naturalWidth > maxWidth) {
|
||||
canvas.width = maxWidth;
|
||||
canvas.height = (image.naturalHeight / image.naturalWidth) * maxWidth;
|
||||
} else {
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
}
|
||||
|
||||
canvas.getContext('2d').drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob((blob) => {
|
||||
$scope.$apply(function () {
|
||||
const myImage = new File([blob], 'background.webp', { type: blob.type });
|
||||
|
||||
$scope.background.cleared = false;
|
||||
$scope.background.newImageFile = myImage;
|
||||
$scope.background.src = URL.createObjectURL(myImage);
|
||||
});
|
||||
}, 'image/webp');
|
||||
|
||||
$scope.background.file = event.target.files[0];
|
||||
};
|
||||
|
||||
image.src = fr.result;
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
$scope.about = {
|
||||
busy: false,
|
||||
error: {},
|
||||
cloudronName: '',
|
||||
avatar: null,
|
||||
avatarBlob: null,
|
||||
|
||||
avatarUrl() {
|
||||
if ($scope.about.avatar) {
|
||||
return $scope.about.avatar.data || $scope.about.avatar.url;
|
||||
} else {
|
||||
return Client.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
refresh() {
|
||||
$scope.about.cloudronName = $scope.config.cloudronName;
|
||||
$scope.about.avatar = null;
|
||||
|
||||
Client.hasCloudronBackground(function (error, result) {
|
||||
if (error) return console.error('Failed to get background state.', error);
|
||||
|
||||
$scope.background.enabled = result;
|
||||
$scope.background.file = null;
|
||||
$scope.background.src = null;
|
||||
$scope.background.newImageFile = null;
|
||||
$scope.background.cacheBusting = Date.now();
|
||||
});
|
||||
},
|
||||
|
||||
submit() {
|
||||
$scope.about.error.name = null;
|
||||
$scope.about.busy = true;
|
||||
|
||||
var NOOP = function (next) { return next(); };
|
||||
var changeCloudronName = $scope.about.cloudronName !== $scope.config.cloudronName ? Client.changeCloudronName.bind(null, $scope.about.cloudronName) : NOOP;
|
||||
|
||||
changeCloudronName(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
if (error.statusCode === 400) {
|
||||
$scope.about.error.cloudronName = error.message || 'Invalid name';
|
||||
$('#inputCloudronName').focus();
|
||||
} else {
|
||||
console.error('Unable to change name.', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var changeAvatar = $scope.about.avatar ? Client.changeCloudronAvatar.bind(null, $scope.about.avatarBlob) : NOOP;
|
||||
|
||||
changeAvatar(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
console.error('Unable to change avatar.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.background.submit(function (error) {
|
||||
if (error) {
|
||||
$scope.about.busy = false;
|
||||
console.error('Unable to change background.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
if ($scope.about.avatar) Client.resetAvatar();
|
||||
|
||||
$scope.aboutForm.$setPristine();
|
||||
$scope.about.avatar = null;
|
||||
$scope.about.refresh();
|
||||
|
||||
$scope.about.busy = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.footer = {
|
||||
content: '',
|
||||
busy: false,
|
||||
|
||||
refresh: function () {
|
||||
Client.getFooter(function (error, result) {
|
||||
if (error) return console.error('Failed to get footer.', error);
|
||||
|
||||
$scope.footer.content = result;
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.footer.busy = true;
|
||||
|
||||
Client.setFooter($scope.footer.content.trim(), function (error) {
|
||||
if (error) return console.error('Failed to set footer.', error);
|
||||
|
||||
Client.refreshConfig(function () {
|
||||
$scope.footer.busy = false;
|
||||
$scope.footerForm.$setPristine();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.about.refresh();
|
||||
$scope.footer.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,535 +0,0 @@
|
||||
<!-- Modal subscription -->
|
||||
<div class="modal fade" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.subscriptionRequired.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.subscriptionRequired.description' | tr"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'domains.subscriptionRequired.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain add/configure -->
|
||||
<div class="modal fade" id="domainConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-show="domainConfigure.adding">{{ 'domains.domainDialog.addTitle' | tr }}</h4>
|
||||
<h4 class="modal-title" ng-hide="domainConfigure.adding">{{ 'domains.domainDialog.editTitle' | tr:{ domain: domainConfigure.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-show="domainConfigure.adding" ng-bind-html="'domains.domainDialog.addDescription' | tr"></p>
|
||||
|
||||
<form name="domainConfigureForm" role="form" novalidate ng-submit="domainConfigure.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainConfigure.error">{{ domainConfigure.error }}</p>
|
||||
|
||||
<div class="form-group" ng-show="domainConfigure.adding">
|
||||
<label class="control-label">{{ 'domains.domainDialog.domain' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.newDomain" name="newDomain" ng-disabled="domainConfigure.busy" placeholder="example.com" ng-required="domainConfigure.adding" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.provider" ng-options="a.value as a.name for a in dnsProvider" ng-change="domainConfigure.setDefaultTlsProvider()"></select>
|
||||
</div>
|
||||
|
||||
<!-- Route53 -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53AccessKeyId' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.accessKeyId" name="accessKeyId" ng-disabled="domainConfigure.busy" ng-minlength="16" ng-maxlength="32" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'route53'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.route53SecretAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.secretAccessKey" name="secretAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'route53'">
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud DNS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gcdns'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gcdnsServiceAccountKey' | tr }}</label>
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcdnsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="domainConfigure.gcdnsKey.keyFileName" id="gcdnsKeyInput" name="cert" onclick="getElementById('gcdnsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gcdns'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcdnsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'digitalocean'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.digitalOceanToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.digitalOceanToken" name="digitalOceanToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'digitalocean'">
|
||||
</div>
|
||||
|
||||
<!-- Gandi -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gandi'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gandiTokenType' | tr }}</label>
|
||||
<select class="form-control" ng-model="domainConfigure.gandiTokenType">
|
||||
<option value="ApiKey">{{ 'domains.domainDialog.gandiTokenTypeApiKey' | tr }}</option>
|
||||
<option value="PAT">{{ 'domains.domainDialog.gandiTokenTypePAT' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'gandi'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.gandiApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.gandiApiKey" name="gandiApiKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'gandi'">
|
||||
</div>
|
||||
|
||||
<!-- GoDaddy -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiKey" name="apiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'godaddy'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.goDaddyApiSecret' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.godaddyApiSecret" name="apiSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'godaddy'">
|
||||
</div>
|
||||
|
||||
<!-- Netcup -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupCustomerNumber' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupCustomerNumber" name="netcupCustomerNumber" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiKey" name="netcupApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'netcup'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.netcupApiPassword' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.netcupApiPassword" name="netcupApiPassword" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'netcup'">
|
||||
</div>
|
||||
|
||||
<!-- OVH -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label" for="inputConfigureOvhEndpoint">{{ 'domains.domainDialog.ovhEndpoint' | tr }}</label>
|
||||
<select class="form-control" name="endpoint" id="inputConfigureOvhEndpoint" ng-model="domainConfigure.ovhEndpoint" ng-options="a.value as a.name for a in ovhEndpoints" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhConsumerKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhConsumerKey" name="ovhConsumerKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhAppKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhAppKey" name="ovhAppKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'ovh'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.ovhAppSecret' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.ovhAppSecret" name="ovhAppSecret" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'ovh'">
|
||||
</div>
|
||||
|
||||
<!-- Porkbun -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'porkbun'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.porkbunApikey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.porkbunApikey" name="porkbunApikey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'porkbun'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'porkbun'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.porkbunSecretapikey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.porkbunSecretapikey" name="porkbunSecretapikey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'porkbun'">
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareTokenType' | tr }}</label>
|
||||
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
|
||||
<option value="GlobalApiKey">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</option>
|
||||
<option value="ApiToken">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'GlobalApiKey'">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</label>
|
||||
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'ApiToken'">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.cloudflareToken" name="cloudflareToken" ng-required="domainConfigure.provider === 'cloudflare'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.cloudflareEmail' | tr }}</label>
|
||||
<input type="email" class="form-control" ng-model="domainConfigure.cloudflareEmail" name="cloudflareEmail" ng-required="domainConfigure.provider === 'cloudflare' && domainConfigure.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="domainConfigure.provider === 'cloudflare'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="domainConfigure.cloudflareDefaultProxyStatus"> {{ 'domains.domainDialog.cloudflareDefaultProxyStatus' | tr }}
|
||||
<sup><a ng-href="https://docs.cloudron.io/domains/#cloudflare-dns" class="help" target="_blank" tabIndex="-1"><i class="fa fa-question-circle"></i></a></sup>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Linode -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'linode'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.linodeToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.linodeToken" name="linodeToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'linode'">
|
||||
</div>
|
||||
|
||||
<!-- Bunny -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'bunny'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.bunnyAccessKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.bunnyAccessKey" name="bunnyAccessKey" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'bunny'">
|
||||
</div>
|
||||
|
||||
<!-- dnsimple -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'dnsimple'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.dnsimpleAccessToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.dnsimpleAccessToken" name="dnsimpleAccessToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'dnsimple'">
|
||||
</div>
|
||||
|
||||
<!-- Hetzner -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'hetzner'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.hetznerToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.hetznerToken" name="hetznerToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'hetzner'">
|
||||
</div>
|
||||
|
||||
<!-- Vultr -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'vultr'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.vultrToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.vultrToken" name="vultrToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'vultr'">
|
||||
</div>
|
||||
|
||||
<!-- deSEC -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'desec'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.deSecToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.deSecToken" name="deSecToken" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'desec'">
|
||||
</div>
|
||||
|
||||
<!-- Name.com -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComUsername" name="nameComUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecom'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.nameComApiToken' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.nameComToken" name="nameComToken" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecom'">
|
||||
</div>
|
||||
|
||||
<!-- Namecheap -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapUsername" name="namecheapUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'namecheap'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.namecheapApiKey' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.namecheapApiKey" name="namecheapApiKey" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'namecheap'">
|
||||
</div>
|
||||
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'namecheap'" ng-bind-html="'domains.domainDialog.namecheapInfo' | tr"></p>
|
||||
|
||||
<!-- INWX -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'inwx'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.inwxUsername' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.inwxUsername" name="inwxUsername" ng-disabled="domainConfigure.busy" ng-required="domainConfigure.provider === 'inwx'">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'inwx'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.inwxPassword' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.inwxPassword" name="inwxPassword" ng-disabled="domainConfigure.busy" ng-minlength="1" ng-required="domainConfigure.provider === 'inwx'">
|
||||
</div>
|
||||
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'wildcard'" ng-bind-html="'domains.domainDialog.wildcardInfo' | tr:{ domain: domainConfigure.adding ? domainConfigure.newDomain : domainConfigure.domain.domain }"></p>
|
||||
<p class="small text-info text-bold" ng-show="domainConfigure.provider === 'manual'" ng-bind-html="'domains.domainDialog.manualInfo' | tr"></p>
|
||||
<p class="small text-info text-bold" ng-show="needsPort80(domainConfigure.provider, domainConfigure.tlsConfig.provider)" ng-bind-html="'domains.domainDialog.letsEncryptInfo' | tr"></p>
|
||||
|
||||
<a href="" ng-click="domainConfigure.advancedVisible = true" ng-hide="domainConfigure.advancedVisible">{{ 'domains.domainDialog.advancedAction' | tr }}</a>
|
||||
<div uib-collapse="!domainConfigure.advancedVisible">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.zoneName' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#zone-name" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="domainConfigure.zoneName" name="zoneName" ng-disabled="domainConfigure.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.certProvider' | tr }} <sup><a ng-href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" ng-model="domainConfigure.tlsConfig.provider" ng-options="a.value as a.name for a in tlsProvider"></select>
|
||||
</div>
|
||||
|
||||
<!-- Fallback certificate -->
|
||||
<div ng-show="domainConfigure.tlsConfig.provider !== 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertInfo' | tr"></p>
|
||||
</div>
|
||||
|
||||
<div ng-show="domainConfigure.tlsConfig.provider === 'fallback'">
|
||||
<label class="control-label">{{ 'domains.domainDialog.fallbackCertCustomCert' | tr }}</label>
|
||||
<p ng-bind-html="'domains.domainDialog.fallbackCertCustomCertInfo' | tr:{ customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates' }"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.cert.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackCertFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertCertificatePlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.certificateFileName" name="cert" onclick="getElementById('fallbackCertFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackCertFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!fallbackCert.key.$dirty && fallbackCert.error) }">
|
||||
<div class="input-group">
|
||||
<input type="file" id="fallbackKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="{{ 'domains.domainDialog.fallbackCertKeyPlaceholder' | tr }}" ng-model="domainConfigure.fallbackCert.keyFileName" id="fallbackKeyInput" name="key" onclick="getElementById('fallbackKeyFileInput').click();" style="cursor: pointer;" ng-disabled="domainConfigure.busy">
|
||||
<span class="input-group-addon"><i class="fa fa-upload" onclick="getElementById('fallbackKeyFileInput').click();"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- advanced -->
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainConfigure.submit()" ng-disabled="domainConfigureForm.$invalid || domainConfigure.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainConfigure.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal domain wellknown -->
|
||||
<div class="modal fade" id="domainWellKnownModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.domainWellKnown.title' | tr:{ domain: domainWellKnown.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.domainDialog.wellKnownDescription' | tr:{ domain: domainWellKnown.domain.domain, docsLink: 'https://docs.cloudron.io/domains/#well-known-locations' }"></p>
|
||||
|
||||
<form name="domainWellKnownForm" role="form" novalidate ng-submit="domainWellKnown.submit()" autocomplete="off">
|
||||
<p class="has-error text-center" ng-show="domainWellKnown.error">{{ domainWellKnown.error }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.matrixHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.matrixHostname" name="matrixHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.mastodonHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.mastodonHostname" name="mastodonHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'domains.domainDialog.jitsiHostname' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="domainWellKnown.jitsiHostname" name="jitsiHostname" ng-disabled="domainWellKnown.busy">
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="domainWellKnown.submit()" ng-disabled="domainWellKnownForm.$invalid || domainWellKnown.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainWellKnown.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal domain remove -->
|
||||
<div class="modal fade" id="domainRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'domains.removeDialog.title' | tr:{ domain: domainRemove.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'domains.removeDialog.description' | tr:{ domain: domainRemove.domain.domain }"></p>
|
||||
<br/>
|
||||
<span class="has-error" ng-show="domainRemove.error">{{ domainRemove.error }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="domainRemove.submit()" ng-disabled="domainRemove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="domainRemove.busy"></i> {{ 'domains.removeDialog.removeAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h1 class="section-header">
|
||||
{{ 'domains.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline" ng-show="domains.length <= pageSize" ng-click="domainAdd.show()"><i class="fa fa-plus"></i> {{ 'domains.addDomain' | tr }}</button>
|
||||
</h1>
|
||||
|
||||
<div class="users-toolbar" ng-show="domains.length > pageSize">
|
||||
<input type="text" id="domainSearchInput" class="form-control" style="max-width: 350px;" ng-model="domainSearchString" placeholder="{{ 'main.searchPlaceholder' | tr }}"/>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="domainAdd.show()"><i class="fa fa-plus"></i> {{ 'domains.addDomain' | tr }}</button>
|
||||
</div>
|
||||
<div class="card card-large">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'domains.domain' | tr }}</th>
|
||||
<th class="text-left hidden-xs hidden-sm">{{ 'domains.provider' | tr }}</th>
|
||||
<th style="width: 100px" class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains | filter:domainSearchString | limitTo:pageSize:((currentPage-1)*pageSize)">
|
||||
<td class="elide-table-cell hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ domain.domain }}
|
||||
</td>
|
||||
<td class="text-left elide-table-cell hidden-xs hidden-sm hand" ng-click="domainConfigure.show(domain)">
|
||||
{{ prettyProviderName(domain) }}
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" uib-tooltip="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" uib-tooltip="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" uib-tooltip="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pull-left">
|
||||
{{ 'domains.count' | tr:{ count: (domains | filter:domainSearchString).length } }}
|
||||
</div>
|
||||
<div class="pull-right" ng-show="domains.length > pageSize">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showPrevPage()" ng-class="{ 'btn-primary': currentPage > 1 }" ng-disabled="currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<span style="margin: 0 5px; line-height: 1.5; font-size: 12px;">{{ currentPage }}</span>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="showNextPage()" ng-class="{ 'btn-primary': (domains | filter:domainSearchString).length > (currentPage * pageSize) }" ng-disabled="(domains | filter:domainSearchString).length <= (currentPage * pageSize)">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">
|
||||
{{ 'domains.renewCerts.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="renewCerts.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in renewCerts.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-bind-html="'domains.renewCerts.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="renewCerts.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ renewCerts.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="renewCerts.busy">{{ renewCerts.message }}</p>
|
||||
<p ng-hide="renewCerts.busy">
|
||||
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">
|
||||
{{ 'domains.syncDns.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="syncDns.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in syncDns.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-bind-html="'domains.syncDns.description' | tr"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="syncDns.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ syncDns.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="syncDns.busy">{{ syncDns.message }}</p>
|
||||
<p ng-hide="syncDns.busy">
|
||||
<div class="has-error" ng-show="!syncDns.active">{{ syncDns.errorMessage }}</div>
|
||||
</p>
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy">{{ 'domains.syncDns.syncAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-header">
|
||||
{{ 'domains.changeDashboardDomain.title' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="changeDashboard.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in changeDashboard.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<p ng-bind-html="'domains.changeDashboardDomain.description' | tr"></p>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" style="background-color: white;">my.</span>
|
||||
<select class="form-control pull-right" style="display: inline-block;" ng-model="changeDashboard.selectedDomain" ng-options="a.domain for a in domains"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="changeDashboard.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ changeDashboard.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p ng-show="changeDashboard.busy">{{ changeDashboard.message }}</p>
|
||||
<p ng-hide="changeDashboard.busy">
|
||||
<div class="has-error" ng-show="!changeDashboard.active">{{ changeDashboard.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
<button class="btn btn-outline btn-primary" ng-click="changeDashboard.change()" ng-hide="changeDashboard.busy" ng-disabled="changeDashboard.selectedDomain.domain === changeDashboard.adminDomain.domain">{{ 'domains.changeDashboardDomain.changeAction' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">{{ 'domains.changeDashboardDomain.cancelAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,800 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global async */
|
||||
/* global angular */
|
||||
/* global $, TASK_TYPES, ENDPOINTS_OVH */
|
||||
|
||||
angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.domains = [];
|
||||
$scope.ready = false;
|
||||
$scope.domainSearchString = '';
|
||||
$scope.pageSize = localStorage.cloudronPageSize || 10;
|
||||
$scope.currentPage = 1;
|
||||
|
||||
$scope.showNextPage = function () {
|
||||
$scope.currentPage++;
|
||||
};
|
||||
|
||||
$scope.showPrevPage = function () {
|
||||
if ($scope.currentPage > 1) $scope.currentPage--;
|
||||
else $scope.currentPage = 1;
|
||||
};
|
||||
|
||||
$scope.translationLinks = {
|
||||
linodeDocsLink: 'https://docs.cloudron.io/domains/#linode-dns',
|
||||
customCertLink: 'https://docs.cloudron.io/certificates/#custom-certificates'
|
||||
};
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
// currently, validation of wildcard with various provider is done server side
|
||||
$scope.tlsProvider = [
|
||||
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
|
||||
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
|
||||
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
|
||||
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
|
||||
{ name: 'Custom Wildcard Certificate', value: 'fallback' },
|
||||
];
|
||||
|
||||
// keep in sync with setup.js
|
||||
$scope.dnsProvider = [
|
||||
{ name: 'AWS Route53', value: 'route53' },
|
||||
{ name: 'Bunny', value: 'bunny' },
|
||||
{ name: 'Cloudflare', value: 'cloudflare' },
|
||||
{ name: 'deSEC', value: 'desec' },
|
||||
{ name: 'DigitalOcean', value: 'digitalocean' },
|
||||
{ name: 'DNSimple', value: 'dnsimple' },
|
||||
{ name: 'Gandi LiveDNS', value: 'gandi' },
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'INWX', value: 'inwx' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
{ name: 'Netcup', value: 'netcup' },
|
||||
{ name: 'OVH', value: 'ovh' },
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
|
||||
$scope.prettyProviderName = function (domain) {
|
||||
switch (domain.provider) {
|
||||
case 'bunny': return 'Bunny';
|
||||
case 'route53': return 'AWS Route53';
|
||||
case 'cloudflare': return 'Cloudflare';
|
||||
case 'desec': return 'deSEC';
|
||||
case 'digitalocean': return 'DigitalOcean';
|
||||
case 'dnsimple': return 'dnsimple';
|
||||
case 'gandi': return 'Gandi LiveDNS';
|
||||
case 'hetzner': return 'Hetzner DNS';
|
||||
case 'inwx': return 'INWX';
|
||||
case 'linode': return 'Linode';
|
||||
case 'namecom': return 'Name.com';
|
||||
case 'namecheap': return 'Namecheap';
|
||||
case 'netcup': return 'Netcup';
|
||||
case 'ovh': return 'OVH';
|
||||
case 'gcdns': return 'Google Cloud';
|
||||
case 'godaddy': return 'GoDaddy';
|
||||
case 'vultr': return 'Vultr';
|
||||
case 'manual': return 'Manual';
|
||||
case 'porkbun': return 'Porkbun';
|
||||
case 'wildcard': return 'Wildcard';
|
||||
case 'noop': return 'No-op';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.ovhEndpoints = ENDPOINTS_OVH;
|
||||
|
||||
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
|
||||
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
|
||||
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
|
||||
};
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
return function (event) {
|
||||
$scope.$apply(function () {
|
||||
obj[file] = null;
|
||||
obj[fileName] = event.target.files[0].name;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (result) {
|
||||
if (!result.target || !result.target.result) return console.error('Unable to read local file');
|
||||
obj[file] = result.target.result;
|
||||
};
|
||||
reader.readAsText(event.target.files[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function refreshDomains(callback) {
|
||||
var domains = [ ];
|
||||
|
||||
Client.getDomains(function (error, results) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
async.eachSeries(results, function (result, iteratorDone) {
|
||||
Client.getDomain(result.domain, function (error, domain) {
|
||||
if (error) return iteratorDone(error);
|
||||
|
||||
domains.push(domain);
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
angular.copy(domains, $scope.domains);
|
||||
|
||||
$scope.changeDashboard.selectedDomain = $scope.changeDashboard.adminDomain = $scope.domains.find(function (d) { return d.domain === $scope.config.adminDomain; });
|
||||
|
||||
if (error) console.error(error);
|
||||
if (callback) callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.domainAdd = {
|
||||
show: function () {
|
||||
$scope.domainConfigure.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainWellKnown = {
|
||||
busy: false,
|
||||
error: null,
|
||||
|
||||
domain: null,
|
||||
mastodonHostname: '',
|
||||
matrixHostname: '',
|
||||
jitsiHostname: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.domainWellKnown.busy = false;
|
||||
$scope.domainWellKnown.error = null;
|
||||
$scope.domainWellKnown.domain = null;
|
||||
|
||||
$scope.domainWellKnown.matrixHostname = '';
|
||||
$scope.domainWellKnown.mastodonHostname = '';
|
||||
$scope.domainWellKnown.jitsiHostname = '';
|
||||
},
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainWellKnown.reset();
|
||||
|
||||
$scope.domainWellKnown.domain = domain;
|
||||
|
||||
try {
|
||||
if (domain.wellKnown && domain.wellKnown['matrix/server']) {
|
||||
$scope.domainWellKnown.matrixHostname = JSON.parse(domain.wellKnown['matrix/server'])['m.server'];
|
||||
}
|
||||
if (domain.wellKnown && domain.wellKnown['host-meta']) {
|
||||
$scope.domainWellKnown.mastodonHostname = domain.wellKnown['host-meta'].match(new RegExp('template="https://(.*?)/'))[1];
|
||||
}
|
||||
if (domain.wellKnown && domain.wellKnown['matrix/client']) {
|
||||
let parsed = JSON.parse(domain.wellKnown['matrix/client']);
|
||||
if (parsed['im.vector.riot.jitsi'] && parsed['im.vector.riot.jitsi']['preferredDomain']) {
|
||||
$scope.domainWellKnown.jitsiHostname = parsed['im.vector.riot.jitsi']['preferredDomain'];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$('#domainWellKnownModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainWellKnown.busy = true;
|
||||
$scope.domainWellKnown.error = null;
|
||||
|
||||
var wellKnown = {};
|
||||
if ($scope.domainWellKnown.matrixHostname) {
|
||||
wellKnown['matrix/server'] = JSON.stringify({ 'm.server': $scope.domainWellKnown.matrixHostname });
|
||||
// https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
|
||||
wellKnown['matrix/client'] = JSON.stringify({
|
||||
'm.homeserver': {
|
||||
'base_url': 'https://' + $scope.domainWellKnown.matrixHostname
|
||||
},
|
||||
'im.vector.riot.jitsi': {
|
||||
'preferredDomain': $scope.domainWellKnown.jitsiHostname
|
||||
}
|
||||
});
|
||||
} else if ($scope.domainWellKnown.jitsiHostname) { // only if matrixHostname is not set
|
||||
wellKnown['matrix/client'] = JSON.stringify({
|
||||
'im.vector.riot.jitsi': {
|
||||
'preferredDomain': $scope.domainWellKnown.jitsiHostname
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.domainWellKnown.mastodonHostname) {
|
||||
wellKnown['host-meta'] = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
+ '<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n'
|
||||
+ '<Link rel="lrdd" type="application/xrd+xml" template="https://' + $scope.domainWellKnown.mastodonHostname + '/.well-known/webfinger?resource={uri}"/>\n'
|
||||
+ '</XRD>';
|
||||
}
|
||||
|
||||
Client.updateDomainWellKnown($scope.domainWellKnown.domain.domain, wellKnown, function (error) {
|
||||
$scope.domainWellKnown.busy = false;
|
||||
if (error) {
|
||||
$scope.domainWellKnown.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#domainWellKnownModal').modal('hide');
|
||||
$scope.domainWellKnown.reset();
|
||||
refreshDomains();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// We reused configure also for adding domains to avoid much code duplication
|
||||
$scope.domainConfigure = {
|
||||
adding: false,
|
||||
error: null,
|
||||
busy: false,
|
||||
domain: null,
|
||||
advancedVisible: false,
|
||||
|
||||
// form model
|
||||
newDomain: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
gcdnsKey: { keyFileName: '', content: '' },
|
||||
digitalOceanToken: '',
|
||||
gandiApiKey: '',
|
||||
gandiTokenType: 'PAT',
|
||||
godaddyApiKey: '',
|
||||
godaddyApiSecret: '',
|
||||
cloudflareToken: '',
|
||||
cloudflareEmail: '',
|
||||
cloudflareDefaultProxyStatus: false,
|
||||
cloudflareTokenType: 'GlobalApiKey',
|
||||
linodeToken: '',
|
||||
bunnyAccessKey: '',
|
||||
dnsimpleAccessToken: '',
|
||||
hetznerToken: '',
|
||||
vultrToken: '',
|
||||
deSecToken: '',
|
||||
nameComToken: '',
|
||||
nameComUsername: '',
|
||||
namecheapUsername: '',
|
||||
namecheapApiKey: '',
|
||||
netcupCustomerNumber: '',
|
||||
netcupApiKey: '',
|
||||
netcupApiPassword: '',
|
||||
ovhEndpoint: 'ovh-eu',
|
||||
ovhConsumerKey: '',
|
||||
ovhAppKey: '',
|
||||
ovhAppSecret: '',
|
||||
porkbunSecretapikey: '',
|
||||
porkbunApikey: '',
|
||||
inwxUsername: '',
|
||||
inwxPassword: '',
|
||||
|
||||
provider: 'route53',
|
||||
zoneName: '',
|
||||
|
||||
tlsConfig: {
|
||||
provider: 'letsencrypt-prod-wildcard'
|
||||
},
|
||||
|
||||
fallbackCert: {
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
},
|
||||
|
||||
setDefaultTlsProvider: function () {
|
||||
var dnsProvider = $scope.domainConfigure.provider;
|
||||
// wildcard LE won't work without automated DNS
|
||||
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
|
||||
} else {
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod-wildcard';
|
||||
}
|
||||
},
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainConfigure.reset();
|
||||
|
||||
if (domain) {
|
||||
$scope.domainConfigure.domain = domain;
|
||||
$scope.domainConfigure.accessKeyId = domain.config.accessKeyId;
|
||||
$scope.domainConfigure.secretAccessKey = domain.config.secretAccessKey;
|
||||
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = '';
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
if (domain.provider === 'gcdns') {
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = domain.config.credentials && domain.config.credentials.client_email;
|
||||
|
||||
$scope.domainConfigure.gcdnsKey.content = JSON.stringify({
|
||||
project_id: domain.config.projectId,
|
||||
client_email: domain.config.credentials.client_email,
|
||||
private_key: domain.config.credentials.private_key
|
||||
});
|
||||
}
|
||||
$scope.domainConfigure.digitalOceanToken = domain.provider === 'digitalocean' ? domain.config.token : '';
|
||||
$scope.domainConfigure.linodeToken = domain.provider === 'linode' ? domain.config.token : '';
|
||||
$scope.domainConfigure.bunnyAccessKey = domain.provider === 'bunny' ? domain.config.accessKey : '';
|
||||
$scope.domainConfigure.dnsimpleAccessToken = domain.provider === 'dnsimple' ? domain.config.accessToken : '';
|
||||
$scope.domainConfigure.hetznerToken = domain.provider === 'hetzner' ? domain.config.token : '';
|
||||
$scope.domainConfigure.vultrToken = domain.provider === 'vultr' ? domain.config.token : '';
|
||||
$scope.domainConfigure.deSecToken = domain.provider === 'desec' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareToken = domain.provider === 'cloudflare' ? domain.config.token : '';
|
||||
$scope.domainConfigure.cloudflareEmail = domain.provider === 'cloudflare' ? domain.config.email : '';
|
||||
$scope.domainConfigure.cloudflareTokenType = domain.provider === 'cloudflare' ? domain.config.tokenType : 'GlobalApiKey';
|
||||
$scope.domainConfigure.cloudflareDefaultProxyStatus = domain.provider === 'cloudflare' ? !!domain.config.defaultProxyStatus : false;
|
||||
|
||||
$scope.domainConfigure.gandiApiKey = domain.provider === 'gandi' ? domain.config.token : '';
|
||||
$scope.domainConfigure.gandiTokenType = domain.provider === 'gandi' ? domain.config.tokenType : 'ApiKey';
|
||||
|
||||
$scope.domainConfigure.godaddyApiKey = domain.provider === 'godaddy' ? domain.config.apiKey : '';
|
||||
$scope.domainConfigure.godaddyApiSecret = domain.provider === 'godaddy' ? domain.config.apiSecret : '';
|
||||
|
||||
$scope.domainConfigure.nameComToken = domain.provider === 'namecom' ? domain.config.token : '';
|
||||
$scope.domainConfigure.nameComUsername = domain.provider === 'namecom' ? domain.config.username : '';
|
||||
|
||||
$scope.domainConfigure.namecheapApiKey = domain.provider === 'namecheap' ? domain.config.token : '';
|
||||
$scope.domainConfigure.namecheapUsername = domain.provider === 'namecheap' ? domain.config.username : '';
|
||||
|
||||
$scope.domainConfigure.inwxUsername = domain.provider === 'inwx' ? domain.config.username : '';
|
||||
$scope.domainConfigure.inwxPassword = domain.provider === 'inwx' ? domain.config.password : '';
|
||||
|
||||
$scope.domainConfigure.netcupCustomerNumber = domain.provider === 'netcup' ? domain.config.customerNumber : '';
|
||||
$scope.domainConfigure.netcupApiKey = domain.provider === 'netcup' ? domain.config.apiKey : '';
|
||||
$scope.domainConfigure.netcupApiPassword = domain.provider === 'netcup' ? domain.config.apiPassword : '';
|
||||
|
||||
$scope.domainConfigure.ovhEndpoint = domain.provider === 'ovh' ? domain.config.endpoint : '';
|
||||
$scope.domainConfigure.ovhConsumerKey = domain.provider === 'ovh' ? domain.config.consumerKey : '';
|
||||
$scope.domainConfigure.ovhAppKey = domain.provider === 'ovh' ? domain.config.appKey : '';
|
||||
$scope.domainConfigure.ovhAppSecret = domain.provider === 'ovh' ? domain.config.appSecret : '';
|
||||
|
||||
$scope.domainConfigure.porkbunApikey = domain.provider === 'porkbun' ? domain.config.apikey : '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = domain.provider === 'porkbun' ? domain.config.secretapikey : '';
|
||||
|
||||
$scope.domainConfigure.provider = domain.provider;
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = domain.tlsConfig.provider;
|
||||
if (domain.tlsConfig.provider.indexOf('letsencrypt') === 0) {
|
||||
if (domain.tlsConfig.wildcard) $scope.domainConfigure.tlsConfig.provider += '-wildcard';
|
||||
}
|
||||
$scope.domainConfigure.zoneName = domain.zoneName;
|
||||
} else {
|
||||
$scope.domainConfigure.adding = true;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainConfigure.busy = true;
|
||||
$scope.domainConfigure.error = null;
|
||||
|
||||
var provider = $scope.domainConfigure.provider;
|
||||
|
||||
var data = {};
|
||||
|
||||
if (provider === 'route53') {
|
||||
data.accessKeyId = $scope.domainConfigure.accessKeyId;
|
||||
data.secretAccessKey = $scope.domainConfigure.secretAccessKey;
|
||||
} else if (provider === 'gcdns') {
|
||||
try {
|
||||
var serviceAccountKey = JSON.parse($scope.domainConfigure.gcdnsKey.content);
|
||||
data.projectId = serviceAccountKey.project_id;
|
||||
data.credentials = {
|
||||
client_email: serviceAccountKey.client_email,
|
||||
private_key: serviceAccountKey.private_key
|
||||
};
|
||||
|
||||
if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) {
|
||||
throw new Error('One or more fields are missing in the JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
$scope.domainConfigure.error = 'Cannot parse Google Service Account Key: ' + e.message;
|
||||
$scope.domainConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
} else if (provider === 'digitalocean') {
|
||||
data.token = $scope.domainConfigure.digitalOceanToken;
|
||||
} else if (provider === 'linode') {
|
||||
data.token = $scope.domainConfigure.linodeToken;
|
||||
} else if (provider === 'bunny') {
|
||||
data.accessKey = $scope.domainConfigure.bunnyAccessKey;
|
||||
} else if (provider === 'dnsimple') {
|
||||
data.accessToken = $scope.domainConfigure.dnsimpleAccessToken;
|
||||
} else if (provider === 'hetzner') {
|
||||
data.token = $scope.domainConfigure.hetznerToken;
|
||||
} else if (provider === 'vultr') {
|
||||
data.token = $scope.domainConfigure.vultrToken;
|
||||
} else if (provider === 'desec') {
|
||||
data.token = $scope.domainConfigure.deSecToken;
|
||||
} else if (provider === 'gandi') {
|
||||
data.token = $scope.domainConfigure.gandiApiKey;
|
||||
data.tokenType = $scope.domainConfigure.gandiTokenType;
|
||||
} else if (provider === 'godaddy') {
|
||||
data.apiKey = $scope.domainConfigure.godaddyApiKey;
|
||||
data.apiSecret = $scope.domainConfigure.godaddyApiSecret;
|
||||
} else if (provider === 'cloudflare') {
|
||||
data.token = $scope.domainConfigure.cloudflareToken;
|
||||
data.email = $scope.domainConfigure.cloudflareEmail;
|
||||
data.tokenType = $scope.domainConfigure.cloudflareTokenType;
|
||||
data.defaultProxyStatus = $scope.domainConfigure.cloudflareDefaultProxyStatus;
|
||||
} else if (provider === 'namecom') {
|
||||
data.token = $scope.domainConfigure.nameComToken;
|
||||
data.username = $scope.domainConfigure.nameComUsername;
|
||||
} else if (provider === 'namecheap') {
|
||||
data.token = $scope.domainConfigure.namecheapApiKey;
|
||||
data.username = $scope.domainConfigure.namecheapUsername;
|
||||
} else if (provider === 'inwx') {
|
||||
data.username = $scope.domainConfigure.inwxUsername;
|
||||
data.password = $scope.domainConfigure.inwxPassword;
|
||||
} else if (provider === 'netcup') {
|
||||
data.customerNumber = $scope.domainConfigure.netcupCustomerNumber;
|
||||
data.apiKey = $scope.domainConfigure.netcupApiKey;
|
||||
data.apiPassword = $scope.domainConfigure.netcupApiPassword;
|
||||
} else if (provider === 'ovh') {
|
||||
data.endpoint = $scope.domainConfigure.ovhEndpoint;
|
||||
data.consumerKey = $scope.domainConfigure.ovhConsumerKey;
|
||||
data.appKey = $scope.domainConfigure.ovhAppKey;
|
||||
data.appSecret = $scope.domainConfigure.ovhAppSecret;
|
||||
} else if (provider === 'porkbun') {
|
||||
data.apikey = $scope.domainConfigure.porkbunApikey;
|
||||
data.secretapikey = $scope.domainConfigure.porkbunSecretapikey;
|
||||
}
|
||||
|
||||
var fallbackCertificate = null;
|
||||
if ($scope.domainConfigure.fallbackCert.certificateFile && $scope.domainConfigure.fallbackCert.keyFile) {
|
||||
fallbackCertificate = {
|
||||
cert: $scope.domainConfigure.fallbackCert.certificateFile,
|
||||
key: $scope.domainConfigure.fallbackCert.keyFile
|
||||
};
|
||||
}
|
||||
|
||||
var tlsConfig = {
|
||||
provider: $scope.domainConfigure.tlsConfig.provider,
|
||||
wildcard: false
|
||||
};
|
||||
if ($scope.domainConfigure.tlsConfig.provider.indexOf('-wildcard') !== -1) {
|
||||
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
|
||||
tlsConfig.wildcard = true;
|
||||
}
|
||||
|
||||
// choose the right api, since we reuse this for adding and configuring domains
|
||||
var func;
|
||||
if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
|
||||
else func = Client.updateDomainConfig.bind(Client, $scope.domainConfigure.domain.domain, $scope.domainConfigure.zoneName, provider, data, fallbackCertificate, tlsConfig);
|
||||
|
||||
func(function (error) {
|
||||
$scope.domainConfigure.busy = false;
|
||||
if (error) {
|
||||
$scope.domainConfigure.error = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#domainConfigureModal').modal('hide');
|
||||
$scope.domainConfigure.reset();
|
||||
|
||||
refreshDomains();
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainConfigure.adding = false;
|
||||
$scope.domainConfigure.advancedVisible = false;
|
||||
$scope.domainConfigure.newDomain = '';
|
||||
|
||||
$scope.domainConfigure.busy = false;
|
||||
$scope.domainConfigure.error = null;
|
||||
|
||||
$scope.domainConfigure.provider = '';
|
||||
$scope.domainConfigure.accessKeyId = '';
|
||||
$scope.domainConfigure.secretAccessKey = '';
|
||||
$scope.domainConfigure.gcdnsKey.keyFileName = '';
|
||||
$scope.domainConfigure.gcdnsKey.content = '';
|
||||
$scope.domainConfigure.digitalOceanToken = '';
|
||||
$scope.domainConfigure.gandiApiKey = '';
|
||||
$scope.domainConfigure.gandiTokenType = 'PAT';
|
||||
$scope.domainConfigure.godaddyApiKey = '';
|
||||
$scope.domainConfigure.godaddyApiSecret = '';
|
||||
$scope.domainConfigure.cloudflareToken = '';
|
||||
$scope.domainConfigure.cloudflareEmail = '';
|
||||
$scope.domainConfigure.cloudflareTokenType = 'GlobalApiKey';
|
||||
$scope.domainConfigure.cloudflareDefaultProxyStatus = false;
|
||||
$scope.domainConfigure.nameComToken = '';
|
||||
$scope.domainConfigure.nameComUsername = '';
|
||||
$scope.domainConfigure.namecheapApiKey = '';
|
||||
$scope.domainConfigure.namecheapUsername = '';
|
||||
$scope.domainConfigure.inwxUsername = '';
|
||||
$scope.domainConfigure.inwxPassword = '';
|
||||
$scope.domainConfigure.netcupCustomerNumber = '';
|
||||
$scope.domainConfigure.netcupApiKey = '';
|
||||
$scope.domainConfigure.netcupApiPassword = '';
|
||||
$scope.domainConfigure.ovhEndpoint = '';
|
||||
$scope.domainConfigure.ovhConsumerKey = '';
|
||||
$scope.domainConfigure.ovhAppKey = '';
|
||||
$scope.domainConfigure.ovhAppSecret = '';
|
||||
$scope.domainConfigure.porkbunApikey = '';
|
||||
$scope.domainConfigure.porkbunSecretapikey = '';
|
||||
$scope.domainConfigure.vultrToken = '';
|
||||
$scope.domainConfigure.deSecToken = '';
|
||||
|
||||
$scope.domainConfigure.tlsConfig.provider = 'letsencrypt-prod';
|
||||
$scope.domainConfigure.zoneName = '';
|
||||
|
||||
$scope.domainConfigureForm.$setPristine();
|
||||
$scope.domainConfigureForm.$setUntouched();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.renewCerts = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.renewCerts.tasks = tasks.slice(0, 10);
|
||||
if ($scope.renewCerts.tasks.length && $scope.renewCerts.tasks[0].active) $scope.renewCerts.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.renewCerts.tasks[0].id;
|
||||
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.renewCerts.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.renewCerts.busy = false;
|
||||
$scope.renewCerts.message = '';
|
||||
$scope.renewCerts.percent = 100; // indicates that 'result' is valid
|
||||
$scope.renewCerts.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.renewCerts.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.renewCerts.busy = true;
|
||||
$scope.renewCerts.percent = data.percent;
|
||||
$scope.renewCerts.message = data.message;
|
||||
window.setTimeout($scope.renewCerts.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
renew: function () {
|
||||
$scope.renewCerts.busy = true;
|
||||
$scope.renewCerts.percent = 0;
|
||||
$scope.renewCerts.message = '';
|
||||
$scope.renewCerts.errorMessage = '';
|
||||
|
||||
// always rebuild the nginx configs when triggered via the UI. we assume user is clicking this because something is wrong
|
||||
Client.renewCerts({ rebuild: true }, function (error /*, taskId */) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.renewCerts.errorMessage = error.message;
|
||||
$scope.renewCerts.busy = false;
|
||||
} else {
|
||||
$scope.renewCerts.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.syncDns = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.syncDns.tasks = tasks.slice(0, 10);
|
||||
if ($scope.syncDns.tasks.length && $scope.syncDns.tasks[0].active) $scope.syncDns.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
var taskId = $scope.syncDns.tasks[0].id;
|
||||
Client.getTask(taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.syncDns.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.syncDns.busy = false;
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.percent = 100; // indicates that 'result' is valid
|
||||
$scope.syncDns.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
$scope.syncDns.refreshTasks(); // update the tasks list dropdown
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.syncDns.busy = true;
|
||||
$scope.syncDns.percent = data.percent;
|
||||
$scope.syncDns.message = data.message;
|
||||
window.setTimeout($scope.syncDns.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
sync: function () {
|
||||
$scope.syncDns.busy = true;
|
||||
$scope.syncDns.percent = 0;
|
||||
$scope.syncDns.message = '';
|
||||
$scope.syncDns.errorMessage = '';
|
||||
|
||||
Client.setDnsRecords({}, function (error /*, taskId */) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.syncDns.errorMessage = error.message;
|
||||
$scope.syncDns.busy = false;
|
||||
} else {
|
||||
$scope.syncDns.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.domainRemove = {
|
||||
busy: false,
|
||||
error: null,
|
||||
domain: null,
|
||||
|
||||
show: function (domain) {
|
||||
$scope.domainRemove.reset();
|
||||
|
||||
$scope.domainRemove.domain = domain;
|
||||
|
||||
$('#domainRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.domainRemove.busy = true;
|
||||
$scope.domainRemove.error = null;
|
||||
|
||||
Client.removeDomain($scope.domainRemove.domain.domain, function (error) {
|
||||
if (error && (error.statusCode === 403 || error.statusCode === 409)) {
|
||||
$scope.domainRemove.error = error.message;
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#domainRemoveModal').modal('hide');
|
||||
$scope.domainRemove.reset();
|
||||
|
||||
refreshDomains();
|
||||
}
|
||||
|
||||
$scope.domainRemove.busy = false;
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.domainRemove.busy = false;
|
||||
$scope.domainRemove.error = null;
|
||||
$scope.domainRemove.domain = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.changeDashboard = {
|
||||
busy: false,
|
||||
percent: 0,
|
||||
message: '',
|
||||
errorMessage: '',
|
||||
taskId: '',
|
||||
selectedDomain: null,
|
||||
adminDomain: null,
|
||||
tasks: [],
|
||||
|
||||
refreshTasks: function () {
|
||||
Client.getTasksByType(TASK_TYPES.TASK_PREPARE_DASHBOARD_LOCATION, function (error, tasks) {
|
||||
if (error) return console.error(error);
|
||||
$scope.changeDashboard.tasks = tasks.slice(0, 10);
|
||||
if ($scope.changeDashboard.tasks.length && $scope.changeDashboard.tasks[0].active) $scope.changeDashboard.updateStatus();
|
||||
});
|
||||
},
|
||||
|
||||
stop: function () {
|
||||
Client.stopTask($scope.changeDashboard.taskId, function (error) {
|
||||
if (error) console.error(error);
|
||||
$scope.changeDashboard.busy = false;
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function () {
|
||||
if (!$scope.changeDashboard.busy) return; // task got stopped
|
||||
|
||||
Client.getTask($scope.changeDashboard.taskId, function (error, data) {
|
||||
if (error) return window.setTimeout($scope.changeDashboard.updateStatus, 5000);
|
||||
|
||||
if (!data.active) {
|
||||
$scope.changeDashboard.busy = false;
|
||||
$scope.changeDashboard.message = '';
|
||||
$scope.changeDashboard.percent = 100; // indicates that 'result' is valid
|
||||
$scope.changeDashboard.errorMessage = data.success ? '' : data.error.message;
|
||||
|
||||
if (!$scope.changeDashboard.errorMessage) $scope.changeDashboard.setDashboardDomain();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.changeDashboard.busy = true;
|
||||
$scope.changeDashboard.percent = data.percent;
|
||||
$scope.changeDashboard.message = data.message;
|
||||
window.setTimeout($scope.changeDashboard.updateStatus, 500);
|
||||
});
|
||||
},
|
||||
|
||||
setDashboardDomain: function () {
|
||||
Client.setDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.changeDashboard.errorMessage = error.message;
|
||||
|
||||
$scope.changeDashboard.busy = false;
|
||||
} else {
|
||||
window.location.href = 'https://my.' + $scope.changeDashboard.selectedDomain.domain;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
change: function () {
|
||||
$scope.changeDashboard.busy = true;
|
||||
$scope.changeDashboard.message = 'Preparing dashboard domain';
|
||||
$scope.changeDashboard.percent = 0;
|
||||
$scope.changeDashboard.errorMessage = '';
|
||||
|
||||
Client.prepareDashboardDomain($scope.changeDashboard.selectedDomain.domain, function (error, taskId) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.changeDashboard.errorMessage = error.message;
|
||||
|
||||
$scope.changeDashboard.busy = false;
|
||||
} else {
|
||||
$scope.changeDashboard.taskId = taskId;
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
refreshDomains(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.ready = true;
|
||||
});
|
||||
|
||||
$scope.renewCerts.refreshTasks();
|
||||
$scope.syncDns.refreshTasks();
|
||||
$scope.changeDashboard.refreshTasks();
|
||||
});
|
||||
|
||||
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName');
|
||||
document.getElementById('fallbackCertFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'certificateFile', 'certificateFileName');
|
||||
document.getElementById('fallbackKeyFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'keyFile', 'keyFileName');
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['domainConfigureModal', 'domainRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,734 +0,0 @@
|
||||
<!-- Modal subscription -->
|
||||
<div class="modal fade" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.subscriptionDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ 'email.subscriptionDialog.description' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'email.subscriptionDialog.setupAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal enable email -->
|
||||
<div class="modal fade" id="enableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.enableEmailDialog.title' | tr:{ domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'email.enableEmailDialog.description' | tr:{ domain: domain.domain, requiredPortsDocsLink: 'https://docs.cloudron.io/email/#required-ports' }"></p>
|
||||
<p class="text-warning" ng-show="domain.provider === 'noop' || domain.provider === 'manual'" ng-bind-html="'email.enableEmailDialog.noProviderInfo' | tr"></p>
|
||||
<p class="text-danger" ng-show="adminDomain.provider === 'cloudflare'" ng-bind-html="'email.enableEmailDialog.cloudflareInfo' | tr:{ adminDomain: config.adminDomain, mailFqdn: config.mailFqdn }"></p>
|
||||
<div ng-hide="domain.provider === 'noop' || domain.provider === 'manual'">
|
||||
<p>
|
||||
<label class="control-label">
|
||||
<input type="checkbox" ng-model="incomingEmail.setupDns"> {{ 'email.enableEmailDialog.setupDnsCheckbox' | tr }}
|
||||
</label>
|
||||
</p>
|
||||
<span ng-bind-html="'email.enableEmailDialog.setupDnsInfo' | tr:{ importEmailDocsLink: 'https://docs.cloudron.io/guides/import-email' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="incomingEmail.enable()">{{ 'email.enableEmailDialog.enableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal disable email -->
|
||||
<div class="modal fade" id="disableEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.disableEmailDialog.title' | tr:{ domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html="'email.disableEmailDialog.description' | tr:{ domain: domain.domain }"></div>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="incomingEmail.disable()">{{ 'email.disableEmailDialog.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add mailbox -->
|
||||
<div class="modal fade" id="mailboxAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.addMailboxDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailboxadd_form" role="form" ng-submit="mailboxes.add.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailboxes.add.error }">
|
||||
<label class="control-label">{{ 'email.addMailboxDialog.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="mailboxes.add.error">
|
||||
<small>{{ mailboxes.add.error.message }}</small>
|
||||
</div>
|
||||
<div class="input-group form-inline" style="margin-top: 10px;">
|
||||
<input type="text" class="form-control" ng-model="mailboxes.add.name" required autofocus autocomplete="off"/>
|
||||
<div class="input-group-addon">@{{ domain.domain }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.addMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.add.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy || !mailboxes.add.owner"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailboxes.add.submit()" ng-disabled="mailboxadd_form.$invalid || mailboxes.add.busy || !mailboxes.add.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.add.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit mailbox -->
|
||||
<div class="modal fade" id="mailboxEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.editMailboxDialog.title' | tr:{ name: mailboxes.edit.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.owner' | tr }}</label>
|
||||
<div class="control-label">
|
||||
<multiselect ng-model="mailboxes.edit.owner" options="o.display for o in owners" data-compare-by="name" data-header-key="header" data-divider-key="divider" data-multiple="false" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group aliases">
|
||||
<label class="control-label">{{ 'email.editMailboxDialog.aliases' | tr }}</label>
|
||||
<div class="has-error" ng-show="mailboxes.edit.error">{{ mailboxes.edit.error.message }}</div>
|
||||
|
||||
<div class="row" ng-repeat="alias in mailboxes.edit.aliases | orderBy:'reversedSortingNotation'">
|
||||
<div class="col col-lg-11">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" ng-model="alias.name">
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
<span>@{{ alias.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="incomingDomain in incomingDomains">
|
||||
<a href="" ng-click="alias.domain = incomingDomain.domain">{{ incomingDomain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-lg-1">
|
||||
<button class="btn btn-danger btn-sm" ng-click="mailboxes.edit.delAlias($event, alias)"><i class="far fa-trash-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length === 0">
|
||||
{{ 'email.editMailboxDialog.noAliases' | tr }} <a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAliasAction' | tr }}</a>
|
||||
</div>
|
||||
<div ng-show="mailboxes.edit.aliases.length > 0" style="margin-top: 5px;">
|
||||
<a href="" ng-click="mailboxes.edit.addAlias($event)">{{ 'email.editMailboxDialog.addAnotherAliasAction' | tr }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="storageQuota">
|
||||
<input id="storageQuota" type="checkbox" ng-model="mailboxes.edit.storageQuotaEnabled">
|
||||
{{ 'email.editMailboxDialog.enableStorageQuota' | tr }} <b ng-hide="!mailboxes.edit.storageQuotaEnabled">: {{ mailboxes.edit.storageQuota | prettyDecimalSize }}</b>
|
||||
</input>
|
||||
</label>
|
||||
<input type="range" id="storageQuota" ng-disabled="!mailboxes.edit.storageQuotaEnabled" ng-model="mailboxes.edit.storageQuota" step="500000000" min="{{ storageQuotaTicks[0] }}" max="{{ storageQuotaTicks[storageQuotaTicks.length-1] }}" list="storageQuotaTicks" />
|
||||
<datalist id="storageQuotaTicks">
|
||||
<option ng-repeat="quota in storageQuotaTicks" value="{{ quota }}"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.enablePop3"> {{ 'email.updateMailboxDialog.enablePop3' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.edit.active"> {{ 'email.updateMailboxDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailboxes.edit.submit()" ng-disabled="mailboxes.edit.busy || !mailboxes.edit.owner"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove mailbox -->
|
||||
<div class="modal fade" id="mailboxRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.deleteMailboxDialog.title' | tr:{ name: mailboxes.remove.mailbox.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html="'email.deleteMailboxDialog.description' | tr"></div>
|
||||
<br/>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailboxes.remove.deleteMails">{{ 'email.deleteMailboxDialog.purgeMailboxCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="mailboxes.remove.submit()" ng-disabled="mailboxes.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxes.remove.busy"></i> {{ 'email.deleteMailboxDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal add mailinglist -->
|
||||
<div class="modal fade" id="mailinglistAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.addMailinglistDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailinglistadd_form" role="form" ng-submit="mailinglists.add.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailinglists.add.error.name }">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.name' | tr }}</label>
|
||||
<div class="control-label" ng-show="mailinglists.add.error.name"><small>{{ mailinglists.add.error.name }}</small></div>
|
||||
<div class="input-group form-inline" style="margin-top: 10px;">
|
||||
<input type="text" class="form-control" ng-model="mailinglists.add.name" required autofocus autocomplete="off"/>
|
||||
<div class="input-group-addon">@{{ domain.domain }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.members' | tr }}</label><br/>
|
||||
<div class="has-error control-label" ng-show="mailinglists.add.error.members"><small>{{ mailinglists.add.error.members }}</small></div>
|
||||
<textarea ng-model="mailinglists.add.membersTxt" class="form-control" rows="5"></textarea>
|
||||
<small>{{ 'email.addMailinglistDialog.membersInfo' | tr }}</small>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.add.membersOnly">{{ 'email.addMailinglistDialog.membersOnlyCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.membersTxt.length === 0 || mailinglists.add.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailinglists.add.submit()" ng-disabled="mailinglistadd_form.$invalid || mailinglists.add.membersTxt.length === 0 || mailinglists.add.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.add.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edit mailinglist -->
|
||||
<div class="modal fade" id="mailinglistEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.editMailinglistDialog.title' | tr:{ name: mailinglists.edit.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="mailinglistedit_form" role="form" ng-submit="mailinglists.edit.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': mailinglists.edit.error.members }">
|
||||
<label class="control-label">{{ 'email.addMailinglistDialog.members' | tr }}</label><br/>
|
||||
<div class="has-error control-label" ng-show="mailinglists.edit.error.members"><small>{{ mailinglists.edit.error.members }}</small></div>
|
||||
<textarea ng-model="mailinglists.edit.membersTxt" class="form-control" rows="5" autofocus></textarea>
|
||||
<small>{{ 'email.addMailinglistDialog.membersInfo' | tr }}</small>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.edit.membersOnly">{{ 'email.addMailinglistDialog.membersOnlyCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailinglists.edit.active"> {{ 'email.updateMailinglistDialog.activeCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="mailinglists.edit.submit()" ng-disabled="mailinglistedit_form.$invalid || mailinglists.edit.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglists.edit.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove mailinglist -->
|
||||
<div class="modal fade" id="mailinglistRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'email.deleteMailinglistDialog.title' | tr:{ name: mailinglists.remove.list.name, domain: domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="'email.deleteMailinglistDialog.description' | tr:{ name: mailinglists.remove.list.name, domain: domain.domain }"></p>`
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="mailinglists.remove.submit()" ng-disabled="mailinglists.remove.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailinglist.remove.busy"></i> {{ 'email.deleteMailinglistDialog.deleteAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal how to connect -->
|
||||
<div class="modal fade" id="howToConnectInfoModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>{{ 'email.howToConnectInfoModal' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html=" 'email.incoming.howToConnectDescription' | tr:{ domain: domain.domain } "></p>
|
||||
|
||||
<p><b>{{ 'email.incoming.incomingUserInfo' | tr }}</b><br/><i>mailboxname</i>@{{ domain.domain }}</p>
|
||||
<p><b>{{ 'email.incoming.incomingPasswordInfo' | tr }}</b><br/>{{ 'email.incoming.incomingPasswordUsage' | tr }}</p>
|
||||
<p><b>{{ 'email.incoming.incomingServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 993 (TLS)</p>
|
||||
<p><b>{{ 'email.incoming.outgointServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 587 (STARTTLS) or 465 (TLS)</p>
|
||||
<p><b>{{ 'email.incoming.sieveServerInfo' | tr }}</b><br/>{{ 'email.incoming.server' | tr }}: <span ng-click-select>{{config.mailFqdn}}</span><br/>{{ 'email.incoming.port' | tr }}: 4190 (STARTTLS)</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!ready" class="loading-banner">
|
||||
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
|
||||
</div>
|
||||
|
||||
<div class="content" ng-show="ready">
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>
|
||||
{{ 'email.config.title' | tr:{ domain: domain.domain } }}
|
||||
|
||||
<div class="dropdown pull-right" style="display: inline-block">
|
||||
<button class="btn btn-sm btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'app.docsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom">
|
||||
<i class="fas fa-book"></i>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="https://docs.cloudron.io/email/" target="_blank">{{ 'app.docsAction' | tr }}</a></li>
|
||||
<li ng-class="{ 'disabled': !domain.mailConfig.enabled }"><a href="" ng-click="domain.mailConfig.enabled ? howToConnectInfo.show() : null">{{ 'email.config.clientConfiguration' | tr }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<uib-tabset active="activeTab">
|
||||
<uib-tab index="'mailboxes'" select="setView('mailboxes')" heading="{{ 'email.incoming.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.incoming.title' | tr }}</h4>
|
||||
|
||||
<p ng-show="domain.mailConfig.enabled">{{ 'email.incoming.enabled' | tr }}</p>
|
||||
<p ng-hide="domain.mailConfig.enabled">{{ 'email.incoming.disabled' | tr }}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="pull-right" ng-class="domain.mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="incomingEmail.toggleEmailEnabled()" ng-disabled="incomingEmail.busy" ng-show="user.isAtLeastAdmin">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.busy"></i>
|
||||
{{ domain.mailConfig.enabled ? ('email.incoming.disableAction' | tr) : ('email.incoming.enableAction' | tr) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">{{ 'email.incoming.mailboxes.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="mailboxes.add.show()" ng-disabled="!domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}"><i class="fa fa-inbox"></i> {{ 'email.incoming.mailboxes.addAction' | tr }}</button>
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailboxes.search" ng-model-options="{ debounce: 1000 }" ng-change="mailboxes.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'email.incoming.mailboxes.name' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.owner' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.aliases' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailboxes.usage' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="mailbox in mailboxes.mailboxes | filter:mailboxes.search" ng-class="{'text-muted': !mailbox.active}">
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
{{ mailbox.name }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailboxes.edit.show(mailbox)">
|
||||
{{ mailbox.ownerDisplayName }}
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="mailboxes.edit.show(mailbox)">
|
||||
<span ng-repeat="alias in mailbox.aliases"> {{ alias.name + '@' + alias.domain }}</span>
|
||||
</td>
|
||||
<td class="hand no-wrap" ng-click="mailboxes.edit.show(mailbox)">
|
||||
<span ng-show="mailUsage !== null">
|
||||
{{ mailUsage[mailbox.fullName].quotaValue | prettyDecimalSize }} <span ng-show="mailUsage[mailbox.fullName].quotaLimit">/ {{ mailUsage[mailbox.fullName].quotaLimit | prettyDecimalSize }}</span>
|
||||
</span>
|
||||
<span ng-show="mailUsage === null">
|
||||
{{ 'main.loadingPlaceholder' | tr }} ...
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="mailboxes.edit.show(mailbox)"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="mailboxes.remove.show(mailbox)"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailboxes.showPrevPage()" ng-class="{ 'btn-primary': mailboxes.currentPage > 1 }" ng-disabled="mailboxes.busy || mailboxes.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailboxes.showNextPage()" ng-class="{ 'btn-primary': mailboxes.perPage <= mailboxes.mailboxes.length }" ng-disabled="mailboxes.busy || mailboxes.perPage > mailboxes.mailboxes.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3 style="margin-bottom: 15px;">{{ 'email.incoming.mailinglists.title' | tr }}
|
||||
<button class="btn btn-primary btn-outline pull-right" ng-click="mailinglists.add.show()" ng-disabled="!domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}"><i class="fa fa-list"></i> {{ 'email.incoming.mailboxes.addAction' | tr }}</button>
|
||||
<input class="form-control pull-right" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="mailinglists.search" ng-model-options="{ debounce: 1000 }" ng-change="mailinglists.updateFilter()" />
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ 'email.incoming.mailinglists.description' | tr }}
|
||||
<br/>
|
||||
<br/>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 0.5%;"></th>
|
||||
<th>{{ 'email.incoming.mailinglists.name' | tr }}</th>
|
||||
<th>{{ 'email.incoming.mailinglists.members' | tr }}</th>
|
||||
<th class="text-right">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="list in mailinglists.mailinglists | filter:mailinglists.search | orderBy:'name'" ng-class="{'text-muted': !list.active}">
|
||||
<td>
|
||||
<i class="fas fa-door-closed" ng-show="list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.membersOnlyTooltip' | tr }}"></i>
|
||||
<i class="fas fa-door-open" ng-show="!list.membersOnly" uib-tooltip="{{ 'email.incoming.mailinglists.everyoneTooltip' | tr }}"></i>
|
||||
</td>
|
||||
<td class="hand" ng-click="mailinglists.edit.show(list)">
|
||||
{{ list.name }}
|
||||
</td>
|
||||
<td class="hand" ng-click="mailinglists.edit.show(list)">
|
||||
{{ list.members.join(', ') }}
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="mailinglists.edit.show(list)"><i class="fa fa-pencil-alt"></i></button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="mailinglists.remove.show(list)"><i class="far fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailinglists.showPrevPage()" ng-class="{ 'btn-primary': mailinglists.currentPage > 1 }" ng-disabled="mailinglists.busy || mailinglists.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline btn-xs" ng-click="mailinglists.showNextPage()" ng-class="{ 'btn-primary': mailinglists.perPage <= mailinglists.mailinglists.length }" ng-disabled="mailinglists.busy || mailinglists.perPage > mailinglists.mailinglists.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-left">
|
||||
<h3>{{ 'email.incoming.catchall.title' | tr }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-bind-html=" 'email.incoming.catchall.description' | tr "></div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<multiselect ng-model="catchall.mailboxes" options="mailbox.display for mailbox in catchall.availableMailboxes" data-compare-by="display" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<button class="btn btn-outline btn-primary" ng-click="catchall.submit()" ng-disabled="catchall.busy || !domain.mailConfig.enabled" tooltip-enable="!domain.mailConfig.enabled" uib-tooltip="{{ 'email.incoming.mailboxes.disabledTooltip' | tr }}">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="catchall.busy"></i> {{ 'email.incoming.catchall.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'outbound'" ng-if="user.isAtLeastAdmin" select="setView('outbound')" heading="{{ 'email.outbound.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.outbound.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#relay-outbound-mails" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-bind-html=" 'email.outbound.description' | tr "></div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<select class="form-control" style="width: 50%;" ng-model="mailRelay.preset" ng-options="a.name for a in mailRelayPresets track by a.provider" ng-change="mailRelay.presetChanged()"></select>
|
||||
</div>
|
||||
|
||||
<p class="small text-danger" ng-show="mailRelay.preset.provider === 'noop'">
|
||||
<span ng-if="domain.domain === config.adminDomain">{{ 'email.outbound.noopAdminDomainWarning' | tr }}</span>
|
||||
<span ng-if="domain.domain !== config.adminDomain">{{ 'email.outbound.noopNonAdminDomainWarning' | tr }}</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="usesExternalServer(mailRelay.preset.provider)">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<form name="mailRelayForm" role="form" ng-submit="mailRelay.submit()" autocomplete="off" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.host' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.host.$dirty && mailRelay.error.host) || (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid)">
|
||||
<small ng-show="!mailRelayForm.host.$dirty && mailRelay.error.host">{{ mailRelay.error.host }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.host" name="host" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.port' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.port.$dirty && mailRelay.error.port) || (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid)">
|
||||
<small ng-show="!mailRelayForm.port.$dirty && mailRelay.error.port">{{ mailRelay.error.port }}</small>
|
||||
</div>
|
||||
<input type="number" class="form-control" ng-model="mailRelay.relay.port" name="port" required>
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="mailRelay.relay.provider === 'external-smtp' || mailRelay.relay.provider === 'external-smtp-noauth'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="mailRelay.relay.acceptSelfSignedCerts">{{ 'email.outbound.mailRelay.selfsignedCheckbox' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Postmark, Sendgrid, SparkPost -->
|
||||
<div ng-show="usesTokenAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.apiTokenOrKey' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken) || (mailRelayForm.serverApiToken.$dirty && mailRelayForm.serverApiToken.$invalid)">
|
||||
<small ng-show="!mailRelayForm.serverApiToken.$dirty && mailRelay.error.serverApiToken">{{ mailRelay.error.serverApiToken }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.serverApiToken" name="serverApiToken" ng-required="usesTokenAuth(mailRelay.relay.provider)">
|
||||
</div>
|
||||
|
||||
<!-- Other -->
|
||||
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.username' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.username.$dirty && mailRelay.error.username) || (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid)">
|
||||
<small ng-show="!mailRelayForm.username.$dirty && mailRelay.error.username">{{ mailRelay.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" ng-required="usesPasswordAuth(mailRelay.relay.provider)">
|
||||
</div>
|
||||
|
||||
<div ng-show="usesPasswordAuth(mailRelay.relay.provider)" class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
|
||||
<label class="control-label">{{ 'email.outbound.mailRelay.password' | tr }}</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.password.$dirty && mailRelay.error.password) || (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid)">
|
||||
<small ng-show="!mailRelayForm.password.$dirty && mailRelay.error.password">{{ mailRelay.error.password }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="mailRelay.relay.password" name="password" ng-required="usesPasswordAuth(mailRelay.relay.provider)" password-reveal>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailRelayForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-primary pull-right" ng-click="mailRelay.submit()" ng-disabled="(usesExternalServer(mailRelay.preset.provider) && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailRelay.busy"></i> {{ 'email.outbound.mailRelay.saveAction' | tr }}</button>
|
||||
|
||||
<span class="has-error text-center" ng-show="mailRelay.error">{{ mailRelay.error }}</span>
|
||||
<span class="text-success text-center text-bold" ng-show="mailRelay.success">{{ 'email.outbound.mailRelay.saveSuccess' | tr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="mailRelay.preset.spfDoc">
|
||||
<br/>
|
||||
<div class="col-md-12">
|
||||
<span class="text-info" ng-bind-html="'email.outbound.mailRelay.spfDocInfo' | tr:{ name: mailRelay.preset.name, spfDocsLink: mailRelay.preset.spfDoc }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'settings'" select="setView('settings')" heading="{{ 'email.settings.tabTitle' | tr }}">
|
||||
<div class="card card-large" style="margin-bottom: 15px;">
|
||||
<h4>{{ 'email.masquerading.title' | tr }}</h4>
|
||||
<p ng-bind-html=" 'email.masquerading.description' | tr "></p>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="pull-right" ng-class="domain.mailConfig.mailFromValidation ? 'btn btn-danger' : 'btn btn-primary'" ng-disabled="mailFromValidation.busy" ng-click="mailFromValidation.submit()">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="mailFromValidation.busy"></i> {{ domain.mailConfig.mailFromValidation ? ('email.masquerading.enableAction' | tr) : ('email.masquerading.disableAction' | tr) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<h4>{{ 'email.signature.title' | tr }}</h4>
|
||||
<p ng-bind-html=" 'email.signature.description' | tr "></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form role="form" name="bannerForm" ng-submit="banner.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">{{ 'email.signature.plainTextFormat' | tr }}</label>
|
||||
<textarea ng-model="banner.text" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" style="width: 100%">{{ 'email.signature.htmlFormat' | tr }}</label>
|
||||
<textarea ng-model="banner.html" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="banner.$invalid || banner.busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-outline btn-primary pull-right" ng-click="banner.submit()" ng-disabled="banner.$invalid || banner.busy">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="banner.busy"></i> {{ 'email.signature.saveAction' | tr }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="'status'" ng-if="user.isAtLeastAdmin" select="setView('status')" heading="{{ 'email.status.tabTitle' | tr }}">
|
||||
<!-- nothing to show if incoming mail is disabled and using a relay -->
|
||||
<div class="card card-large" style="margin-bottom: 15px;" ng-hide="!domain.mailConfig.enabled && domain.mailConfig.relay.provider !== 'cloudron-smtp'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ 'email.dnsStatus.title' | tr }}
|
||||
<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="incomingEmail.setDnsRecords()">
|
||||
<i class="fa fa-circle-notch fa-spin" ng-show="incomingEmail.setupDnsBusy"></i> {{ 'email.dnsStatus.reSetupAction' | tr }}
|
||||
</button>
|
||||
</h4>
|
||||
<span ng-bind-html="'email.dnsStatus.description' | tr:{ emailDnsDocsLink:'https://docs.cloudron.io/email/#dns-records'}"></span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div ng-repeat="record in expectedDnsRecordsTypes">
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value].expected">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p ng-show="record.name === 'MX' && domain.provider === 'namecheap'">{{ 'email.dnsStatus.namecheapInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#namecheap-dns" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR4' || record.name === 'PTR6'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.hostname' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].name }}</tt></b></p>
|
||||
<p ng-hide="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.domain' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
|
||||
<p>{{ 'email.dnsStatus.type' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">{{ 'email.dnsStatus.expected' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].expected }}</tt></b></p>
|
||||
<p style="overflow: auto; white-space: nowrap;">{{ 'email.dnsStatus.current' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : ('['+('email.dnsStatus.recordNotSet' | tr)+']') }}</tt></b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large" style="margin-bottom: 15px;" ng-if="domain.mailConfig.relay.provider !== 'noop'">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ 'email.smtpStatus.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#smtp-status" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="domain.mailStatus.relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_outbound_smtp">
|
||||
{{ domain.mailConfig.relay.provider === 'cloudron-smtp' ? ('email.smtpStatus.outboudDirect' | tr) : ('email.smtpStatus.outboudRelay' | tr) }}
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!domain.mailStatus.relay.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_outbound_smtp" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ domain.mailStatus.relay.value }} </b> </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="domain.mailConfig.relay.provider === 'cloudron-smtp'">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-hide="refreshBusy" ng-class="domain.mailStatus.rbl.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_rbl">{{ 'email.smtpStatus.blacklistCheck' | tr }}</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="refreshStatus()" ng-disabled="refreshBusy" ng-show="!domain.mailStatus.rbl.status"><i class="fa fa-sync-alt" ng-class="{ 'fa-pulse': refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_rbl" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div ng-show="domain.mailStatus.rbl.servers.length" ng-bind-html="'email.smtpStatus.blacklisted' | tr:{ ip: domain.mailStatus.rbl.ip }"></div>
|
||||
<div ng-hide="domain.mailStatus.rbl.servers.length" ng-bind-html="'email.smtpStatus.notBlacklisted' | tr:{ ip: domain.mailStatus.rbl.ip }"></div>
|
||||
<div ng-repeat="server in domain.mailStatus.rbl.servers">
|
||||
<a ng-href="{{server.site}}" target="_blank">{{ server.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</div>
|
||||
@@ -1,898 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global angular */
|
||||
/* global $ */
|
||||
/* global async */
|
||||
|
||||
angular.module('Application').controller('EmailController', ['$scope', '$location', '$translate', '$timeout', '$route', '$routeParams', 'Client', function ($scope, $location, $translate, $timeout, $route, $routeParams, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastMailManager) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
// Avoid full reload on path change
|
||||
// https://stackoverflow.com/a/22614334
|
||||
// reloadOnUrl: false in $routeProvider did not work!
|
||||
var lastRoute = $route.current;
|
||||
$scope.$on('$locationChangeSuccess', function (/* event */) {
|
||||
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
|
||||
$route.current = lastRoute;
|
||||
}
|
||||
});
|
||||
|
||||
var domainName = $routeParams.domain;
|
||||
if (!domainName) return $location.path('/email');
|
||||
|
||||
$scope.setView = function (view, setAlways) {
|
||||
if (!setAlways && !$scope.ready) return;
|
||||
if ($scope.view === view) return;
|
||||
|
||||
$route.updateParams({ view: view });
|
||||
$scope.view = view;
|
||||
$scope.activeTab = view;
|
||||
};
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.refreshBusy = true;
|
||||
$scope.client = Client;
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.apps = Client.getInstalledApps();
|
||||
$scope.owners = []; // users + groups
|
||||
$scope.incomingDomains = [];
|
||||
$scope.domain = null;
|
||||
$scope.adminDomain = null;
|
||||
$scope.mailUsage = null;
|
||||
$scope.storageQuotaTicks = [ 500*1000*1000, 5*1000*1000*1000, 15*1000*1000*1000, 50*1000*1000*1000, 100*1000*1000*1000 ];
|
||||
|
||||
$scope.expectedDnsRecords = {
|
||||
mx: {},
|
||||
dkim: {},
|
||||
spf: {},
|
||||
dmarc: {},
|
||||
ptr4: {},
|
||||
ptr6: {}
|
||||
};
|
||||
|
||||
$scope.expectedDnsRecordsTypes = [
|
||||
{ name: 'MX', value: 'mx' },
|
||||
{ name: 'DKIM', value: 'dkim' },
|
||||
{ name: 'SPF', value: 'spf' },
|
||||
{ name: 'DMARC', value: 'dmarc' },
|
||||
{ name: 'PTR4', value: 'ptr4' },
|
||||
{ name: 'PTR6', value: 'ptr6' }
|
||||
|
||||
];
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
Client.openSubscriptionSetup($scope.$parent.subscription);
|
||||
};
|
||||
|
||||
function updateMailUsage(mailboxName, quotaLimit) {
|
||||
if (!$scope.mailUsage) $scope.mailUsage = {};
|
||||
if (!$scope.mailUsage[mailboxName]) $scope.mailUsage[mailboxName] = {};
|
||||
$scope.mailUsage[mailboxName].quotaLimit = quotaLimit;
|
||||
}
|
||||
|
||||
function refreshMailUsage() {
|
||||
Client.getMailUsage($scope.domain.domain, function (error, usage) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$scope.mailUsage = usage || null; // if mail server is down, don't stop the listing
|
||||
});
|
||||
}
|
||||
|
||||
$scope.catchall = {
|
||||
mailboxes: [],
|
||||
availableMailboxes: [],
|
||||
busy: false,
|
||||
|
||||
submit: function () {
|
||||
$scope.catchall.busy = true;
|
||||
|
||||
var addresses = $scope.catchall.mailboxes.map(function (m) { return m.name + '@' + m.domain; });
|
||||
|
||||
Client.setCatchallAddresses($scope.domain.domain, addresses, function (error) {
|
||||
if (error) console.error('Unable to add catchall address.', error);
|
||||
|
||||
$timeout(function () { $scope.catchall.busy = false; }, 2000); // otherwise, it's too fast
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
var allMailboxes = [];
|
||||
async.eachSeries($scope.incomingDomains, function (domain, iteratorDone) {
|
||||
|
||||
Client.listMailboxes(domain.domain, '', 1, 1000, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
result.forEach(function (r) { r.display = r.name + '@' + r.domain; });
|
||||
|
||||
allMailboxes = allMailboxes.concat(result);
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
$scope.catchall.availableMailboxes = allMailboxes;
|
||||
|
||||
$scope.catchall.mailboxes = $scope.domain.mailConfig.catchAll.map(function (address) {
|
||||
return allMailboxes.find(function (m) { return m.display === address; });
|
||||
}).filter(function (m) { return !!m; }); // remove not found addresses
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailinglists = {
|
||||
busy: false,
|
||||
mailinglists: [],
|
||||
search: '',
|
||||
currentPage: 1,
|
||||
perPage: 10,
|
||||
|
||||
add: {
|
||||
busy: false,
|
||||
error: {},
|
||||
name: '',
|
||||
membersTxt: '',
|
||||
membersOnly: false,
|
||||
|
||||
reset: function () {
|
||||
$scope.mailinglists.add.busy = false;
|
||||
$scope.mailinglists.add.error = {};
|
||||
$scope.mailinglists.add.name = '';
|
||||
$scope.mailinglists.add.membersTxt = '';
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.mailinglists.add.reset();
|
||||
$('#mailinglistAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailinglists.add.busy = true;
|
||||
|
||||
var members = $scope.mailinglists.add.membersTxt
|
||||
.split(/[\n,]/)
|
||||
.map(function (m) { return m.trim(); })
|
||||
.filter(function (m) { return m.length !== 0; });
|
||||
|
||||
Client.addMailingList($scope.domain.domain, $scope.mailinglists.add.name, members, $scope.mailinglists.add.membersOnly, function (error) {
|
||||
$scope.mailinglists.add.busy = false;
|
||||
$scope.mailinglists.add.error = {};
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 400 && error.message.indexOf('member') !== -1) {
|
||||
$scope.mailinglists.add.error.members = error.message;
|
||||
} else {
|
||||
$scope.mailinglists.add.error.name = error.message;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailinglists.add.reset();
|
||||
$scope.mailinglists.refresh();
|
||||
|
||||
$('#mailinglistAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
busy: false,
|
||||
error: {},
|
||||
name: '',
|
||||
membersTxt: '',
|
||||
membersOnly: false,
|
||||
active: true,
|
||||
|
||||
show: function (list) {
|
||||
$scope.mailinglists.edit.name = list.name;
|
||||
$scope.mailinglists.edit.membersTxt = list.members.sort().join('\n');
|
||||
$scope.mailinglists.edit.membersOnly = list.membersOnly;
|
||||
$scope.mailinglists.edit.active = list.active;
|
||||
|
||||
$('#mailinglistEditModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailinglists.edit.busy = true;
|
||||
|
||||
var members = $scope.mailinglists.edit.membersTxt.split(/[\n,]/)
|
||||
.map(function (m) { return m.trim(); })
|
||||
.filter(function (m) { return m.length !== 0; });
|
||||
|
||||
Client.updateMailingList($scope.domain.domain, $scope.mailinglists.edit.name, members, $scope.mailinglists.edit.membersOnly, $scope.mailinglists.edit.active, function (error) {
|
||||
$scope.mailinglists.edit.busy = false;
|
||||
$scope.mailinglists.edit.error = {};
|
||||
|
||||
if (error) {
|
||||
$scope.mailinglists.edit.error.members = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailinglists.refresh();
|
||||
|
||||
$('#mailinglistEditModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
remove: {
|
||||
busy: false,
|
||||
list: null,
|
||||
|
||||
show: function (list) {
|
||||
$scope.mailinglists.remove.list = list;
|
||||
|
||||
$('#mailinglistRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailinglists.remove.busy = true;
|
||||
|
||||
Client.removeMailingList($scope.domain.domain, $scope.mailinglists.remove.list.name, function (error) {
|
||||
$scope.mailinglists.remove.busy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mailinglists.remove.list = null;
|
||||
$scope.mailinglists.refresh();
|
||||
|
||||
$('#mailinglistRemoveModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (callback) {
|
||||
callback = typeof callback === 'function' ? callback : function (error) { if (error) return console.error(error); };
|
||||
|
||||
Client.listMailingLists($scope.domain.domain, $scope.mailinglists.search, $scope.mailinglists.currentPage, $scope.mailinglists.perPage, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.mailinglists.mailinglists = result;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.mailinglists.currentPage++;
|
||||
$scope.mailinglists.refresh();
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.mailinglists.currentPage > 1) $scope.mailinglists.currentPage--;
|
||||
else $scope.mailinglists.currentPage = 1;
|
||||
$scope.mailinglists.refresh();
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.mailinglists.currentPage = 1;
|
||||
$scope.mailinglists.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailFromValidation = {
|
||||
busy: false,
|
||||
|
||||
submit: function () {
|
||||
$scope.mailFromValidation.busy = true;
|
||||
|
||||
Client.setMailFromValidation($scope.domain.domain, !$scope.domain.mailConfig.mailFromValidation, function (error) {
|
||||
if (error) {
|
||||
$scope.mailFromValidation.busy = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// give sometime for the mail container to restart
|
||||
$timeout(function () {
|
||||
$scope.mailFromValidation.busy = false;
|
||||
$scope.refreshDomain();
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.banner = {
|
||||
busy: false,
|
||||
text: '',
|
||||
html: '',
|
||||
|
||||
submit: function () {
|
||||
$scope.banner.busy = true;
|
||||
|
||||
Client.setMailBanner($scope.domain.domain, { text: $scope.banner.text, html: $scope.banner.html }, function (error) {
|
||||
if (error) {
|
||||
$scope.banner.busy = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// give sometime for the mail container to restart
|
||||
$timeout(function () {
|
||||
$scope.banner.busy = false;
|
||||
$scope.refreshDomain();
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.incomingEmail = {
|
||||
busy: false,
|
||||
setupDns: true,
|
||||
setupDnsBusy: false,
|
||||
|
||||
toggleEmailEnabled: function () {
|
||||
if ($scope.domain.mailConfig.enabled) {
|
||||
$('#disableEmailModal').modal('show');
|
||||
} else {
|
||||
$('#enableEmailModal').modal('show');
|
||||
}
|
||||
},
|
||||
|
||||
setDnsRecords: function (callback) {
|
||||
$scope.incomingEmail.setupDnsBusy = true;
|
||||
|
||||
Client.setDnsRecords({ domain: $scope.domain.domain, type: 'mail' }, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
$timeout(function () { $scope.incomingEmail.setupDnsBusy = false; }, 2000); // otherwise, it's too fast
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
},
|
||||
|
||||
enable: function () {
|
||||
$('#enableEmailModal').modal('hide');
|
||||
|
||||
$scope.incomingEmail.busy = true;
|
||||
|
||||
Client.enableMailForDomain($scope.domain.domain, true , function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.reconfigureEmailApps();
|
||||
|
||||
let maybeSetupDns = $scope.incomingEmail.setupDns ? $scope.incomingEmail.setDnsRecords : function (next) { next(); };
|
||||
|
||||
maybeSetupDns(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$timeout(function () {
|
||||
$scope.refreshDomain();
|
||||
$scope.incomingEmail.busy = false;
|
||||
}, 5000); // wait for mail container to restart. it cannot get IP otherwise while refreshing
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
disable: function () {
|
||||
$('#disableEmailModal').modal('hide');
|
||||
|
||||
$scope.incomingEmail.busy = true;
|
||||
|
||||
Client.enableMailForDomain($scope.domain.domain, false , function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.reconfigureEmailApps();
|
||||
|
||||
$timeout(function () {
|
||||
$scope.refreshDomain();
|
||||
$scope.incomingEmail.busy = false;
|
||||
}, 5000); // wait for mail container to restart. it cannot get IP otherwise while refreshing
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailboxes = {
|
||||
mailboxes: [],
|
||||
search: '',
|
||||
currentPage: 1,
|
||||
perPage: 10,
|
||||
|
||||
add: {
|
||||
error: null,
|
||||
busy: false,
|
||||
name: '',
|
||||
owner: null,
|
||||
|
||||
reset: function () {
|
||||
$scope.mailboxes.add.busy = false;
|
||||
$scope.mailboxes.add.error = null;
|
||||
$scope.mailboxes.add.name = '';
|
||||
$scope.mailboxes.add.owner = null;
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.mailboxes.add.reset();
|
||||
$('#mailboxAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailboxes.add.busy = true;
|
||||
|
||||
Client.addMailbox($scope.domain.domain, $scope.mailboxes.add.name, $scope.mailboxes.add.owner.id, $scope.mailboxes.add.owner.type, function (error) {
|
||||
if (error) {
|
||||
$scope.mailboxes.add.busy = false;
|
||||
$scope.mailboxes.add.error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.mailboxes.refresh();
|
||||
$scope.catchall.refresh();
|
||||
|
||||
$('#mailboxAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
busy: false,
|
||||
error: null,
|
||||
name: '',
|
||||
owner: null,
|
||||
aliases: [],
|
||||
aliasesOriginal: [],
|
||||
active: true,
|
||||
enablePop3: false,
|
||||
storageQuota: 0,
|
||||
storageQuotaEnabled: false,
|
||||
|
||||
addAlias: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
$scope.mailboxes.edit.aliases.push({
|
||||
name: '',
|
||||
domain: domainName,
|
||||
reversedSortingNotation: 'z'.repeat(100) // quick and dirty to ensure newly added are on bottom
|
||||
});
|
||||
},
|
||||
|
||||
delAlias: function (event, alias) {
|
||||
event.preventDefault();
|
||||
|
||||
var index = $scope.mailboxes.edit.aliases.findIndex(function (a) { return (a.name+a.domain) === (alias.name+alias.domain); });
|
||||
if (index === -1) return;
|
||||
|
||||
$scope.mailboxes.edit.aliases.splice(index, 1);
|
||||
},
|
||||
|
||||
show: function (mailbox) {
|
||||
$scope.mailboxes.edit.name = mailbox.name;
|
||||
$scope.mailboxes.edit.owner = mailbox.owner; // this can be null if mailbox had no owner
|
||||
$scope.mailboxes.edit.aliases = angular.copy(mailbox.aliases, []).map(function (a) { a.reversedSortingNotation = a.domain + '@' + a.name; return a; });
|
||||
$scope.mailboxes.edit.active = mailbox.active;
|
||||
$scope.mailboxes.edit.enablePop3 = mailbox.enablePop3;
|
||||
$scope.mailboxes.edit.storageQuotaEnabled = !!mailbox.storageQuota;
|
||||
$scope.mailboxes.edit.storageQuota = mailbox.storageQuota || $scope.storageQuotaTicks[0];
|
||||
|
||||
// make a copy for later comparison
|
||||
angular.copy($scope.mailboxes.edit.aliases, $scope.mailboxes.edit.aliasesOriginal);
|
||||
|
||||
$('#mailboxEditModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailboxes.edit.busy = true;
|
||||
|
||||
var data = {
|
||||
ownerId: $scope.mailboxes.edit.owner.id,
|
||||
ownerType: $scope.mailboxes.edit.owner.type,
|
||||
active: $scope.mailboxes.edit.active,
|
||||
enablePop3: $scope.mailboxes.edit.enablePop3,
|
||||
storageQuota: $scope.mailboxes.edit.storageQuotaEnabled ? parseInt($scope.mailboxes.edit.storageQuota) : 0,
|
||||
messagesQuota: 0
|
||||
};
|
||||
|
||||
// $scope.mailboxes.edit.owner is expected to be validated by the UI
|
||||
Client.updateMailbox($scope.domain.domain, $scope.mailboxes.edit.name, data, function (error) {
|
||||
if (error) {
|
||||
$scope.mailboxes.edit.error = error;
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
function done() {
|
||||
updateMailUsage($scope.mailboxes.edit.name + '@' + $scope.domain.domain, $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0); // hack to avoid refresh
|
||||
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
$scope.mailboxes.edit.error = null;
|
||||
$scope.mailboxes.edit.name = '';
|
||||
$scope.mailboxes.edit.owner = null;
|
||||
$scope.mailboxes.edit.aliases = [];
|
||||
$scope.mailboxes.edit.storageQuota = 0;
|
||||
$scope.mailboxes.edit.storageQuotaEnabled = false;
|
||||
$scope.mailboxes.refresh();
|
||||
|
||||
$('#mailboxEditModal').modal('hide');
|
||||
}
|
||||
|
||||
// skip if nothing has changed
|
||||
if (angular.equals($scope.mailboxes.edit.aliases, $scope.mailboxes.edit.aliasesOriginal)) return done();
|
||||
|
||||
Client.setAliases($scope.mailboxes.edit.name, $scope.domain.domain, $scope.mailboxes.edit.aliases, function (error) {
|
||||
if (error) {
|
||||
$scope.mailboxes.edit.error = error;
|
||||
$scope.mailboxes.edit.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
remove: {
|
||||
busy: false,
|
||||
mailbox: null,
|
||||
deleteMails: true,
|
||||
|
||||
show: function (mailbox) {
|
||||
$scope.mailboxes.remove.mailbox = mailbox;
|
||||
|
||||
$('#mailboxRemoveModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailboxes.remove.busy = true;
|
||||
|
||||
Client.removeMailbox($scope.domain.domain, $scope.mailboxes.remove.mailbox.name, $scope.mailboxes.remove.deleteMails, function (error) {
|
||||
$scope.mailboxes.remove.busy = false;
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mailboxes.remove.mailbox = null;
|
||||
$scope.mailboxes.refresh(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.catchall.refresh();
|
||||
|
||||
$('#mailboxRemoveModal').modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (callback) {
|
||||
callback = typeof callback === 'function' ? callback : function (error) { if (error) return console.error(error); };
|
||||
|
||||
Client.listMailboxes($scope.domain.domain, $scope.mailboxes.search, $scope.mailboxes.currentPage, $scope.mailboxes.perPage, function (error, mailboxes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxes.forEach(function (m) {
|
||||
m.fullName = m.name + '@' + m.domain; // to make it simple for the ui
|
||||
m.owner = $scope.owners.find(function (o) { return o.id === m.ownerId; }); // owner may not exist
|
||||
m.ownerDisplayName = m.owner ? m.owner.display : ''; // this meta property is set when we get the user list
|
||||
});
|
||||
|
||||
$scope.mailboxes.mailboxes = mailboxes;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.mailboxes.currentPage++;
|
||||
$scope.mailboxes.refresh();
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.mailboxes.currentPage > 1) $scope.mailboxes.currentPage--;
|
||||
else $scope.mailboxes.currentPage = 1;
|
||||
$scope.mailboxes.refresh();
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.mailboxes.currentPage = 1;
|
||||
$scope.mailboxes.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailRelayPresets = [
|
||||
{ provider: 'cloudron-smtp', name: 'Built-in SMTP server' },
|
||||
{ provider: 'external-smtp', name: 'External SMTP server', host: '', port: 587 },
|
||||
{ provider: 'external-smtp-noauth', name: 'External SMTP server (No Authentication)', host: '', port: 587 },
|
||||
{ provider: 'ses-smtp', name: 'Amazon SES', host: 'email-smtp.us-east-1.amazonaws.com', port: 587, spfDoc: 'https://docs.aws.amazon.com/ses/latest/DeveloperGuide/spf.html' },
|
||||
{ provider: 'elasticemail-smtp', name: 'Elastic Email', host: 'smtp.elasticemail.com', port: 587, spfDoc: 'https://elasticemail.com/blog/marketing_tips/common-spf-errors' },
|
||||
{ provider: 'google-smtp', name: 'Google', host: 'smtp.gmail.com', port: 587, spfDoc: 'https://support.google.com/a/answer/33786?hl=en' },
|
||||
{ provider: 'mailgun-smtp', name: 'Mailgun', host: 'smtp.mailgun.org', port: 587, spfDoc: 'https://www.mailgun.com/blog/white-labeling-dns-records-your-customers-tips-tricks' },
|
||||
{ provider: 'mailjet-smtp', name: 'Mailjet', host: '', port: 587, spfDoc: 'https://app.mailjet.com/docs/spf-dkim-guide' },
|
||||
{ provider: 'office365-legacy-smtp', name: 'Office 365', host: 'smtp-legacy.office365.com', port: 587 }, // uses "login" AUTH instead of "plain"
|
||||
{ provider: 'postmark-smtp', name: 'Postmark', host: 'smtp.postmarkapp.com', port: 587, spfDoc: 'https://postmarkapp.com/support/article/1092-how-do-i-set-up-spf-for-postmark' },
|
||||
{ provider: 'sendgrid-smtp', name: 'SendGrid', host: 'smtp.sendgrid.net', port: 587, username: 'apikey', spfDoc: 'https://sendgrid.com/docs/ui/account-and-settings/spf-records/' },
|
||||
{ provider: 'sparkpost-smtp', name: 'SparkPost', host: 'smtp.sparkpostmail.com', port: 587, username: 'SMTP_Injection', spfDoc: 'https://www.sparkpost.com/resources/email-explained/spf-sender-policy-framework/' },
|
||||
{ provider: 'noop', name: 'Disable' },
|
||||
];
|
||||
|
||||
$scope.usesTokenAuth = function (provider) {
|
||||
return provider === 'postmark-smtp' || provider === 'sendgrid-smtp' || provider === 'sparkpost-smtp';
|
||||
};
|
||||
|
||||
$scope.usesExternalServer = function (provider) {
|
||||
return provider !== 'cloudron-smtp' && provider !== 'noop';
|
||||
};
|
||||
|
||||
$scope.usesPasswordAuth = function (provider) {
|
||||
return provider === 'external-smtp'
|
||||
|| provider === 'ses-smtp'
|
||||
|| provider === 'google-smtp'
|
||||
|| provider === 'mailgun-smtp'
|
||||
|| provider === 'elasticemail-smtp'
|
||||
|| provider === 'office365-legacy-smtp'
|
||||
|| provider === 'mailjet-smtp';
|
||||
};
|
||||
|
||||
$scope.mailRelay = {
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
preset: $scope.mailRelayPresets[0],
|
||||
|
||||
// form data to be set on load
|
||||
relay: {
|
||||
provider: 'cloudron-smtp',
|
||||
host: '',
|
||||
port: 25,
|
||||
username: '',
|
||||
password: '',
|
||||
serverApiToken: '',
|
||||
acceptSelfSignedCerts: false
|
||||
},
|
||||
|
||||
presetChanged: function () {
|
||||
$scope.mailRelay.error = null;
|
||||
|
||||
$scope.mailRelay.relay.provider = $scope.mailRelay.preset.provider;
|
||||
$scope.mailRelay.relay.host = $scope.mailRelay.preset.host;
|
||||
$scope.mailRelay.relay.port = $scope.mailRelay.preset.port;
|
||||
$scope.mailRelay.relay.username = '';
|
||||
$scope.mailRelay.relay.password = '';
|
||||
$scope.mailRelay.relay.serverApiToken = '';
|
||||
$scope.mailRelay.relay.acceptSelfSignedCerts = false;
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailRelay.error = null;
|
||||
$scope.mailRelay.busy = true;
|
||||
$scope.mailRelay.success = false;
|
||||
|
||||
var data = {
|
||||
provider: $scope.mailRelay.relay.provider,
|
||||
host: $scope.mailRelay.relay.host,
|
||||
port: $scope.mailRelay.relay.port,
|
||||
acceptSelfSignedCerts: $scope.mailRelay.relay.acceptSelfSignedCerts,
|
||||
forceFromAddress: false
|
||||
};
|
||||
|
||||
// fill in provider specific username/password usage
|
||||
if (data.provider === 'postmark-smtp') {
|
||||
data.username = $scope.mailRelay.relay.serverApiToken;
|
||||
data.password = $scope.mailRelay.relay.serverApiToken;
|
||||
data.forceFromAddress = true; // postmark requires the "From:" in mail to be a Sender Signature
|
||||
} else if (data.provider === 'sendgrid-smtp') {
|
||||
data.username = 'apikey';
|
||||
data.password = $scope.mailRelay.relay.serverApiToken;
|
||||
} else if (data.provider === 'sparkpost-smtp') {
|
||||
data.username = 'SMTP_Injection';
|
||||
data.password = $scope.mailRelay.relay.serverApiToken;
|
||||
} else {
|
||||
data.username = $scope.mailRelay.relay.username;
|
||||
data.password = $scope.mailRelay.relay.password;
|
||||
}
|
||||
|
||||
Client.setMailRelay($scope.domain.domain, data, function (error) {
|
||||
|
||||
if (error) {
|
||||
$scope.mailRelay.error = error.message;
|
||||
$scope.mailRelay.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.domain.relay = data;
|
||||
|
||||
// let the mail server restart, otherwise we get "Error getting IP" errors during refresh
|
||||
$timeout(function () {
|
||||
$scope.mailRelay.busy = false;
|
||||
$scope.mailRelay.success = true;
|
||||
$scope.refreshDomain();
|
||||
|
||||
// clear success indicator after 5sec
|
||||
$timeout(function () { $scope.mailRelay.success = false; }, 5000);
|
||||
}, 5000);
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function resetDnsRecords() {
|
||||
$scope.expectedDnsRecordsTypes.forEach(function (record) {
|
||||
var type = record.value;
|
||||
$scope.expectedDnsRecords[type] = {};
|
||||
|
||||
$('#collapse_dns_' + type).collapse('hide');
|
||||
});
|
||||
|
||||
$('#collapse_outbound_smtp').collapse('hide');
|
||||
$('#collapse_rbl').collapse('hide');
|
||||
}
|
||||
|
||||
function showExpectedDnsRecords() {
|
||||
// open the record details if they are not correct
|
||||
$scope.expectedDnsRecordsTypes.forEach(function (record) {
|
||||
var type = record.value;
|
||||
$scope.expectedDnsRecords[type] = $scope.domain.mailStatus.dns[type] || {};
|
||||
|
||||
if (!$scope.expectedDnsRecords[type].status) {
|
||||
$('#collapse_dns_' + type).collapse('show');
|
||||
}
|
||||
});
|
||||
|
||||
if (!$scope.domain.mailStatus.relay.status) {
|
||||
$('#collapse_outbound_smtp').collapse('show');
|
||||
}
|
||||
|
||||
if (!$scope.domain.mailStatus.rbl.status) {
|
||||
$('#collapse_rbl').collapse('show');
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectDomain = function () {
|
||||
$location.path('/email/' + $scope.domain.domain, false);
|
||||
};
|
||||
|
||||
// this is required because we need to rewrite the CLOUDRON_MAIL_SERVER_HOST env var
|
||||
$scope.reconfigureEmailApps = function () {
|
||||
var installedApps = Client.getInstalledApps();
|
||||
for (var i = 0; i < installedApps.length; i++) {
|
||||
if (!installedApps[i].manifest.addons.email) continue;
|
||||
|
||||
Client.repairApp(installedApps[i].id, { }, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.refreshDomain = function () {
|
||||
$scope.refreshBusy = true;
|
||||
|
||||
resetDnsRecords();
|
||||
|
||||
Client.getMailConfigForDomain(domainName, function (error, mailConfig) {
|
||||
if (error && error.statusCode === 404) return $location.path('/email');
|
||||
if (error) {
|
||||
$scope.refreshBusy = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// pre-fill the form
|
||||
$scope.mailRelay.relay.provider = mailConfig.relay.provider;
|
||||
$scope.mailRelay.relay.host = mailConfig.relay.host;
|
||||
$scope.mailRelay.relay.port = mailConfig.relay.port;
|
||||
$scope.mailRelay.relay.acceptSelfSignedCerts = !!mailConfig.relay.acceptSelfSignedCerts;
|
||||
$scope.mailRelay.relay.username = '';
|
||||
$scope.mailRelay.relay.password = '';
|
||||
$scope.mailRelay.relay.serverApiToken = '';
|
||||
|
||||
if (mailConfig.relay.provider === 'postmark-smtp') {
|
||||
$scope.mailRelay.relay.serverApiToken = mailConfig.relay.username;
|
||||
} else if (mailConfig.relay.provider === 'sendgrid-smtp') {
|
||||
$scope.mailRelay.relay.serverApiToken = mailConfig.relay.password;
|
||||
} else if (mailConfig.relay.provider === 'sparkpost-smtp') {
|
||||
$scope.mailRelay.relay.serverApiToken = mailConfig.relay.password;
|
||||
} else {
|
||||
$scope.mailRelay.relay.username = mailConfig.relay.username;
|
||||
$scope.mailRelay.relay.password = mailConfig.relay.password;
|
||||
}
|
||||
|
||||
for (var i = 0; i < $scope.mailRelayPresets.length; i++) {
|
||||
if ($scope.mailRelayPresets[i].provider === mailConfig.relay.provider) {
|
||||
$scope.mailRelay.preset = $scope.mailRelayPresets[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.banner.text = mailConfig.banner.text || '';
|
||||
$scope.banner.html = mailConfig.banner.html || '';
|
||||
|
||||
// amend to selected domain to be available for the UI
|
||||
$scope.domain.mailConfig = mailConfig;
|
||||
$scope.domain.mailStatus = {};
|
||||
|
||||
refreshMailUsage();
|
||||
$scope.mailboxes.refresh();
|
||||
$scope.mailinglists.refresh();
|
||||
$scope.catchall.refresh();
|
||||
|
||||
// we will fetch the status without blocking the ui
|
||||
Client.getMailStatusForDomain($scope.domain.domain, function (error, mailStatus) {
|
||||
$scope.refreshBusy = false;
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.domain.mailStatus = mailStatus;
|
||||
|
||||
showExpectedDnsRecords();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.refreshStatus = function () {
|
||||
$scope.refreshBusy = true;
|
||||
|
||||
Client.getMailStatusForDomain($scope.domain.domain, function (error, mailStatus) {
|
||||
if (error) {
|
||||
$scope.refreshBusy = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// overwrite the selected domain status to be available for the UI
|
||||
$scope.domain.mailStatus = mailStatus;
|
||||
|
||||
showExpectedDnsRecords();
|
||||
|
||||
$scope.refreshBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.howToConnectInfo = {
|
||||
show: function () {
|
||||
$('#howToConnectInfoModal').modal('show');
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.isAdminDomain = $scope.config.adminDomain === domainName;
|
||||
|
||||
Client.getAllUsers(function (error, users) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
// ensure we have a display value available
|
||||
$scope.owners.push({ header: true, display: $translate.instant('email.mailboxboxDialog.usersHeader') });
|
||||
users.forEach(function (u) {
|
||||
$scope.owners.push({ display: u.username || u.email, id: u.id, type: 'user' });
|
||||
});
|
||||
|
||||
Client.getGroups(function (error, groups) {
|
||||
if (error) return console.error('Unable to get group listing.', error);
|
||||
|
||||
$scope.owners.push({ header: true, display: $translate.instant('email.mailboxboxDialog.groupsHeader') });
|
||||
groups.forEach(function (g) {
|
||||
$scope.owners.push({ display: g.name, id: g.id, type: 'group' });
|
||||
});
|
||||
|
||||
$scope.owners.push({ header: true, display: $translate.instant('email.mailboxboxDialog.appsHeader') });
|
||||
Client.getInstalledApps().forEach(function (a) {
|
||||
if (a.manifest.addons && a.manifest.addons.recvmail) $scope.owners.push({ display: a.label || a.fqdn, id: a.id, type: 'app' });
|
||||
});
|
||||
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) return console.error('Unable to list domains.', error);
|
||||
|
||||
$scope.domain = result.filter(function (d) { return d.domain === domainName; })[0];
|
||||
$scope.adminDomain = result.filter(function (d) { return d.domain === $scope.config.adminDomain; })[0];
|
||||
|
||||
async.eachSeries(result, function (domain, iteratorDone) {
|
||||
Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) {
|
||||
if (error) return console.error('Failed to fetch mail config for domain', domain.domain, error);
|
||||
|
||||
if (mailConfig.enabled) $scope.incomingDomains.push(domain);
|
||||
iteratorDone();
|
||||
});
|
||||
}, function iteratorDone(error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.refreshDomain(); // this calls catchall.refresh() which in turn relies on incomingDomains
|
||||
|
||||
$scope.setView($routeParams.view || 'mailboxes', true /* always set */);
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['mailboxAddModal', 'mailboxEditModal', 'mailinglistEditModal', 'mailinglistAddModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find('[autofocus]:first').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,100 +0,0 @@
|
||||
<div>
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h1>
|
||||
{{ 'emails.eventlog.title' | tr }}
|
||||
|
||||
<a class="btn btn-default btn-outline pull-right" href="/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
<a class="btn btn-default btn-outline pull-right" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="maillog-filter">
|
||||
<input class="form-control" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="activity.search" ng-model-options="{ debounce: 1000 }" ng-change="activity.updateFilter(true)" />
|
||||
<multiselect ng-model="activity.selectedTypes" ms-header="{{ 'emails.typeFilterHeader' | tr }}" options="a.name for a in activityTypes" data-multiple="true" ng-change="activity.updateFilter(true)" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
<select class="form-control" ng-model="activity.pageItems" ng-options="a.name for a in pageItemCount" ng-change="activity.updateFilter(true)"></select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.refresh()"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': busyRefresh }"></i></button>
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showPrevPage()" ng-disabled="activity.busy || activity.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="activity.showNextPage()" ng-disabled="activity.busy || activity.perPage > activity.eventLogs.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="card card-block" style="max-width: 100%">
|
||||
<div>
|
||||
<center ng-show="activity.busy"><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center>
|
||||
<table ng-hide="activity.busy" class="table table-hover" style="margin: 0; table-layout:fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%"><!-- Icon --></th>
|
||||
<th style="width: 15%">{{ 'emails.eventlog.time' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.eventlog.mailFrom' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.eventlog.rcptTo' | tr }}</th>
|
||||
<th style="width: 30%">{{ 'emails.eventlog.details' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-hide="activity.eventLogs.length">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<br>
|
||||
<br>
|
||||
{{ 'emails.eventlog.empty' | tr }}
|
||||
<br>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-show="activity.eventLogs.length" ng-repeat="eventlog in activity.eventLogs">
|
||||
<tr ng-click="activity.showEventLogDetails(eventlog)" class="hand">
|
||||
<td class="no-wrap">
|
||||
<i class="fas fa-arrow-circle-left" ng-show="eventlog.type === 'delivered'" uib-tooltip="{{ 'emails.eventlog.type.outgoing' | tr }}"></i>
|
||||
<i class="fas fa-history" ng-show="eventlog.type === 'deferred'" uib-tooltip="{{ 'emails.eventlog.type.deferred' | tr }}"></i>
|
||||
<i class="fas fa-arrow-circle-right" ng-show="eventlog.type === 'received'" uib-tooltip="{{ 'emails.eventlog.type.incoming' | tr }}"></i>
|
||||
<i class="fas fa-align-justify" ng-show="eventlog.type === 'queued' && eventlog.spamStatus.indexOf('Yes,') !== 0" uib-tooltip="{{ 'emails.eventlog.type.queued' | tr }}"></i>
|
||||
<i class="fas fa-trash" ng-show="eventlog.type === 'queued' && eventlog.spamStatus.indexOf('Yes,') === 0" uib-tooltip="{{ 'emails.eventlog.type.queued' | tr }}"></i>
|
||||
<i class="fas fa-minus-circle" ng-show="eventlog.type === 'denied'" uib-tooltip="{{ 'emails.eventlog.type.denied' | tr }}"></i>
|
||||
<i class="fas fa-hand-paper" ng-show="eventlog.type === 'bounce'" uib-tooltip="{{ 'emails.eventlog.type.bounce' | tr }}"></i>
|
||||
<i class="fas fa-filter" ng-show="eventlog.type === 'spam-learn'" uib-tooltip="{{ 'emails.eventlog.type.spamFilterTrained' | tr }}"></i>
|
||||
<i class="fas fa-fill-drip" ng-show="eventlog.type === 'quota'" uib-tooltip="{{ 'emails.eventlog.type.quota' | tr }}"></i>
|
||||
</td>
|
||||
<td class="no-wrap"><span uib-tooltip="{{ eventlog.ts | prettyLongDate }}" class="arrow">{{ eventlog.ts | prettyDate }}</span></td>
|
||||
<td class="elide-table-cell">{{ (eventlog.mailFrom | prettyEmailAddresses) || '-' }}</td>
|
||||
<td class="elide-table-cell">{{ (eventlog.rcptTo | prettyEmailAddresses) || eventlog.mailbox || '-' }}</td>
|
||||
<td>
|
||||
<span ng-show="eventlog.type === 'bounce'">{{ 'emails.eventlog.type.bounceInfo' | tr }}. {{ eventlog.message || eventlog.reason }}</span>
|
||||
<span ng-show="eventlog.type === 'deferred'">{{ 'emails.eventlog.type.deferredInfo' | tr: { delay:eventlog.delay } }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span ng-show="eventlog.type === 'queued'">
|
||||
<span ng-show="eventlog.direction === 'inbound'">{{ 'emails.eventlog.type.inboundInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.direction === 'outbound'">{{ 'emails.eventlog.type.outboundInfo' | tr }}</span>
|
||||
</span>
|
||||
<span ng-show="eventlog.type === 'received'">{{ 'emails.eventlog.type.receivedInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'delivered'">{{ 'emails.eventlog.type.deliveredInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'denied'">{{ 'emails.eventlog.type.deniedInfo' | tr }}. {{ eventlog.message || eventlog.reason }} </span>
|
||||
<span ng-show="eventlog.type === 'spam-learn'">{{ 'emails.eventlog.type.spamFilterTrainedInfo' | tr }}</span>
|
||||
<span ng-show="eventlog.type === 'quota'">
|
||||
<span ng-show="eventlog.quotaPercent > 0">{{ 'emails.eventlog.type.overQuotaInfo' | tr: { mailbox: eventlog.mailbox, quotaPercent: eventlog.quotaPercent } }}</span>
|
||||
<span ng-show="eventlog.quotaPercent < 0">{{ 'emails.eventlog.type.underQuotaInfo' | tr: { mailbox: eventlog.mailbox, quotaPercent: -eventlog.quotaPercent } }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="activity.activeEventLog === eventlog">
|
||||
<td colspan="6">
|
||||
<pre class="eventlog-details">{{ eventlog | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,83 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $ */
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('EmailsEventlogController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
$scope.activityTypes = [
|
||||
{ name: 'Bounce', value: 'bounce' },
|
||||
{ name: 'Deferred', value: 'deferred' },
|
||||
{ name: 'Delivered', value: 'delivered' },
|
||||
{ name: 'Denied', value: 'denied' },
|
||||
{ name: 'Queued', value: 'queued' },
|
||||
{ name: 'Quota', value: 'quota' },
|
||||
{ name: 'Received', value: 'received' },
|
||||
{ name: 'Spam', value: 'spam' },
|
||||
];
|
||||
|
||||
$scope.activity = {
|
||||
busy: true,
|
||||
eventLogs: [],
|
||||
activeEventLog: null,
|
||||
currentPage: 1,
|
||||
perPage: 20,
|
||||
pageItems: $scope.pageItemCount[0],
|
||||
selectedTypes: [],
|
||||
search: '',
|
||||
|
||||
refresh: function () {
|
||||
$scope.activity.busy = true;
|
||||
|
||||
var types = $scope.activity.selectedTypes.map(function (a) { return a.value; }).join(',');
|
||||
|
||||
Client.getMailEventLogs($scope.activity.search, types, $scope.activity.currentPage, $scope.activity.pageItems.value, function (error, result) {
|
||||
if (error) return console.error('Failed to fetch mail eventlogs.', error);
|
||||
|
||||
$scope.activity.busy = false;
|
||||
|
||||
$scope.activity.eventLogs = result;
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.activity.currentPage++;
|
||||
$scope.activity.refresh();
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.activity.currentPage > 1) $scope.activity.currentPage--;
|
||||
else $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
},
|
||||
|
||||
showEventLogDetails: function (eventLog) {
|
||||
if ($scope.activity.activeEventLog === eventLog) $scope.activity.activeEventLog = null;
|
||||
else $scope.activity.activeEventLog = eventLog;
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.activity.currentPage = 1;
|
||||
$scope.activity.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.ready = true;
|
||||
|
||||
$scope.activity.refresh();
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,81 +0,0 @@
|
||||
<div>
|
||||
<a href="/#/email" class="back-to-view-link"><i class="fas fa-arrow-left"></i> {{ 'email.backAction' | tr }}</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h1>
|
||||
{{ 'emails.queue.title' | tr }}
|
||||
|
||||
<a class="btn btn-default btn-outline pull-right" href="/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="maillog-filter">
|
||||
<input class="form-control" style="width: 200px;" placeholder="{{ 'main.searchPlaceholder' | tr }}" type="text" ng-model="queue.search" ng-model-options="{ debounce: 1000 }" ng-change="queue.updateFilter(true)" />
|
||||
<select class="form-control" ng-model="queue.pageItems" ng-options="a.name for a in pageItemCount" ng-change="queue.updateFilter(true)"></select>
|
||||
</div>
|
||||
<div class="pagination pull-right">
|
||||
<button class="btn btn-default btn-outline" ng-click="queue.reload()"><i class="fas fa-sync-alt" ng-class="{ 'fa-spin': queue.busyRefresh }"></i></button>
|
||||
<button class="btn btn-default btn-outline" ng-click="queue.showPrevPage()" ng-disabled="queue.busy || queue.currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
|
||||
<button class="btn btn-default btn-outline" ng-click="queue.showNextPage()" ng-disabled="queue.busy || queue.perPage > queue.items.length">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="card card-block" style="max-width: 100%">
|
||||
<div>
|
||||
<center ng-show="queue.busy"><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center>
|
||||
<table ng-hide="queue.busy" class="table table-hover" style="margin: 0; table-layout:fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%">{{ 'emails.queue.queueTime' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.queue.mailFrom' | tr }}</th>
|
||||
<th style="width: 25%">{{ 'emails.queue.rcptTo' | tr }}</th>
|
||||
<th style="width: 30%">{{ 'emails.queue.details' | tr }}</th>
|
||||
<th class="text-right" style="width: 5%">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-hide="queue.items.length">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">
|
||||
<br>
|
||||
<br>
|
||||
{{ 'emails.queue.empty' | tr }}
|
||||
<br>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-show="queue.items.length" ng-repeat="item in queue.items">
|
||||
<tr ng-click="queue.showItemDetails(item)" class="hand">
|
||||
<td class="no-wrap"><span uib-tooltip="{{ item.queueTime | prettyLongDate }}" class="arrow">{{ item.queueTime | prettyDate }}</span></td>
|
||||
<td class="elide-table-cell">{{ (item.mailFrom | prettyEmailAddresses) || '-' }}</td>
|
||||
<td class="elide-table-cell">{{ (item.rcptTo | prettyEmailAddresses) || '-' }}</td>
|
||||
<td class="elide-table-cell">
|
||||
<span ng-show="item.queueType === 'delivery'">Delivering</span>
|
||||
<span ng-show="item.queueType === 'tempfail'">Retrying in {{ item.nextAttemptTime | prettyFutureDate }}. {{ item.attempts+1 }} attempt(s) so far.</span>
|
||||
<span ng-show="item.queueType === 'load'">Loading</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<!-- resend is broken in haraka -->
|
||||
<!-- <button class="btn btn-xs btn-default" ng-click="queue.resend(item)" uib-tooltip="{{ 'emails.queue.resendTooltip' | tr }}"><i class="fa fa-retweet"></i></button> -->
|
||||
<button class="btn btn-xs btn-default" ng-show="item.queueType === 'tempfail'" ng-click="$event.stopPropagation(); queue.discard(item)" uib-tooltip="{{ 'emails.queue.discardTooltip' | tr }}"><i class="fa fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="queue.activeItem === item">
|
||||
<td colspan="6">
|
||||
<pre class="item-details">{{ item | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,95 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* global $ */
|
||||
/* global angular */
|
||||
|
||||
angular.module('Application').controller('EmailsQueueController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.pageItemCount = [
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
||||
{ name: $translate.instant('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
||||
];
|
||||
|
||||
$scope.queue = {
|
||||
busy: true,
|
||||
busyRefresh: false,
|
||||
items: [],
|
||||
activeItem: null,
|
||||
currentPage: 1,
|
||||
perPage: 20,
|
||||
pageItems: $scope.pageItemCount[0],
|
||||
search: '',
|
||||
|
||||
refresh: function (showBusy, callback) {
|
||||
if (showBusy) $scope.queue.busy = true;
|
||||
|
||||
Client.listMailQueue($scope.queue.search, $scope.queue.currentPage, $scope.queue.pageItems.value, function (error, result) {
|
||||
if (showBusy) $scope.queue.busy = false;
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to fetch mail eventlogs.', error);
|
||||
} else {
|
||||
$scope.queue.items = result;
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
},
|
||||
|
||||
reload: function () {
|
||||
$scope.queue.busyRefresh = true;
|
||||
$scope.queue.refresh(true, function () {
|
||||
$scope.queue.busyRefresh = false;
|
||||
});
|
||||
},
|
||||
|
||||
resend: function (item) {
|
||||
Client.resendMailQueueItem(item.file, function (error) {
|
||||
if (error) return console.error('Failed to retry item.', error);
|
||||
$scope.queue.refresh(false);
|
||||
});
|
||||
},
|
||||
|
||||
discard: function (item) {
|
||||
Client.delMailQueueItem(item.file, function (error) {
|
||||
if (error) return console.error('Failed to discard item.', error);
|
||||
$scope.queue.refresh(false);
|
||||
});
|
||||
},
|
||||
|
||||
showNextPage: function () {
|
||||
$scope.queue.currentPage++;
|
||||
$scope.queue.refresh(true);
|
||||
},
|
||||
|
||||
showPrevPage: function () {
|
||||
if ($scope.queue.currentPage > 1) $scope.queue.currentPage--;
|
||||
else $scope.queue.currentPage = 1;
|
||||
$scope.queue.refresh(true);
|
||||
},
|
||||
|
||||
showItemDetails: function (item) {
|
||||
if ($scope.queue.activeItem === item) $scope.queue.activeItem = null;
|
||||
else $scope.queue.activeItem = item;
|
||||
},
|
||||
|
||||
updateFilter: function (fresh) {
|
||||
if (fresh) $scope.queue.currentPage = 1;
|
||||
$scope.queue.refresh(false);
|
||||
}
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.ready = true;
|
||||
|
||||
$scope.queue.refresh(true);
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -1,354 +0,0 @@
|
||||
<!-- Modal change max email size -->
|
||||
<div class="modal fade" id="maxEmailSizeChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.changeMailSizeDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeMailSizeDialog.description' | tr "></div>
|
||||
<br>
|
||||
<form name="maxEmailSizeChangeForm" role="form" novalidate ng-submit="maxEmailSize.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="maxEmailSizeInput">{{ 'emails.changeMailSizeDialog.size' | tr }} <b>{{ maxEmailSize.size | prettyDecimalSize }}</b></label>
|
||||
<input type="range" id="maxEmailSizeInput" ng-model="maxEmailSize.size" step="1000000" min="1000000" max="1000000000" />
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="maxEmailSize.submit()" ng-disabled="maxEmailSize.size === maxEmailSize.currentSize"><i class="fa fa-circle-notch fa-spin" ng-show="maxEmailSize.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change virtual all mail -->
|
||||
<div class="modal fade" id="virtualAllMailChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.changeVirtualAllMailDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html=" 'emails.changeVirtualAllMailDialog.description' | tr "></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button class="btn btn-primary" ng-click="virtualAllMail.submit(!virtualAllMail.enabled)"><i class="fa fa-circle-notch fa-spin" ng-show="virtualAllMail.busy"></i> {{ virtualAllMail.enabled ? ('main.disableAction' | tr) : ('main.enableAction' | tr) }} </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal solr config -->
|
||||
<div class="modal fade" id="ftsConfigModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.solrConfig.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html=" 'emails.solrConfig.description' | tr "></p>
|
||||
<!-- only show this when user is trying to enable -->
|
||||
<p class="has-error" ng-show="!ftsConfig.currentConfig.enabled && !ftsConfig.enoughMemory">{{ 'emails.solrConfig.notEnoughMemory' | tr }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-hide="ftsConfig.currentConfig.enabled" ng-click="ftsConfig.submit(true)" ng-disabled="(!ftsConfig.currentConfig.enabled && !ftsConfig.enoughMemory) || ftsConfig.busy"><i class="fa fa-circle-notch fa-spin" ng-show="ftsConfig.busy"></i> {{ 'main.enableAction' | tr }}</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="ftsConfig.currentConfig.enabled" ng-click="ftsConfig.submit(false)" ng-disabled="ftsConfig.busy"><i class="fa fa-circle-notch fa-spin" ng-show="ftsConfig.busy"></i> {{ 'main.disableAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change acl -->
|
||||
<div class="modal fade" id="aclChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.aclDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="aclChangeForm" role="form" novalidate ng-submit="acl.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.aclDialog.dnsblZones' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#dnsbl" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<p class="small">{{ 'emails.aclDialog.dnsblZonesInfo' | tr }}</p>
|
||||
<div class="has-error" ng-show="acl.error.dnsblZones">{{ acl.error.dnsblZones }}</div>
|
||||
<textarea ng-model="acl.dnsblZones" placeholder="{{ 'emails.aclDialog.dnsblZonesPlaceholder' | tr }}" name="dnsblZones" class="form-control" ng-class="{ 'has-error': !aclChangeForm.dnsblZones.$dirty && acl.error.dnsblZones }" rows="4"></textarea>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="acl.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="acl.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change spam config -->
|
||||
<div class="modal fade" id="spamConfigChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.spamFilterDialog.title' | tr }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="spamConfigChangeForm" role="form" novalidate ng-submit="spamConfig.submit()" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.spamFilterDialog.blacklisteAddresses' | tr }}</label>
|
||||
<p class="small">{{ 'emails.spamFilterDialog.blacklisteAddressesInfo' | tr }}</p>
|
||||
<div class="has-error" ng-show="spamConfig.error.blocklist">{{ spamConfig.error.blocklist }}</div>
|
||||
<textarea ng-model="spamConfig.blocklist" placeholder="{{ 'emails.spamFilterDialog.blacklisteAddressesPlaceholder' | tr }}" name="blocklist" class="form-control" ng-class="{ 'has-error': !spamConfigChangeForm.blocklist.$dirty && spamConfig.error.blocklist }" rows="4"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">{{ 'emails.spamFilterDialog.customRules' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#custom-spam-filtering-rules" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<div class="has-error" ng-show="spamConfig.error.config">{{ spamConfig.error.config }}</div>
|
||||
<textarea ng-model="spamConfig.config" placeholder="{{ 'emails.spamFilterDialog.customRulesPlaceholder' | tr }}" class="form-control" name="config" ng-class="{ 'has-error': !spamConfigChangeForm.config.$dirty && spamConfig.error.config }" rows="4"></textarea>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="button" class="btn btn-success" ng-click="spamConfig.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="spamConfig.busy"></i> {{ 'main.dialog.save' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test email -->
|
||||
<div class="modal fade" id="testEmailModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ 'emails.testMailDialog.title' | tr:{ domain: testEmail.domain.domain } }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="testEmailForm" role="form" novalidate ng-submit="testEmail.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="testEmail.error">{{ testEmail.error.generic }}</p>
|
||||
<p ng-bind-html="'emails.testMailDialog.description' | tr:{ domain: testEmail.domain.domain }"></p>
|
||||
<br/>
|
||||
<div class="form-group" ng-class="{ 'has-error': testEmail.error.key }">
|
||||
<label class="control-label" for="inputTestEmailKey">{{ 'emails.testMailDialog.mailTo' | tr }}</label>
|
||||
<input type="text" class="form-control" ng-model="testEmail.mailTo" id="inputTestMailTo" name="mailTo" ng-disabled="testEmail.busy" placeholder="{{ 'emails.testMailDialog.mailToPlaceholder' | tr }}" autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="testEmailForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="testEmail.submit()" ng-disabled="testEmail.$invalid || testEmail.busy"><i class="fa fa-circle-notch fa-spin" ng-show="testEmail.busy"></i> {{ 'emails.testMailDialog.sendAction' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h1 class="section-header">
|
||||
{{ 'emails.title' | tr }}
|
||||
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastAdmin" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
|
||||
<a class="btn btn-default" ng-show="user.isAtLeastAdmin" href="#/emails-eventlog">{{ 'eventlog.title' | tr }}</a>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<!-- domain listing -->
|
||||
<h3 class="section-header">{{ 'emails.domains.title' | tr }}</h3>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row ng-hide" ng-hide="ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-circle-notch fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-hover" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10px"></th>
|
||||
<th>{{ 'emails.domains.domain' | tr }}</th>
|
||||
<th class="hide-mobile">{{ 'emails.domains.config' | tr }}</th>
|
||||
<th style="text-align: right;">{{ 'main.actions' | tr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="domain in domains">
|
||||
<td>
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': domain.statusOk, 'status-error': !domain.statusOk }" ng-show="domain.status"></i>
|
||||
<i class="fa fa-circle-notch fa-spin" ng-hide="domain.status"></i>
|
||||
</td>
|
||||
<td class="elide-table-cell no-padding">
|
||||
<a href="/#/email/{{ domain.domain }}" class="email-domain-list-item">{{ domain.domain }}</a>
|
||||
</td>
|
||||
<td class="elide-table-cell no-padding hide-mobile">
|
||||
<a href="/#/email/{{ domain.domain }}" class="email-domain-list-item">
|
||||
<span ng-switch on="domain.loading">
|
||||
<span ng-switch-when="true">{{ 'main.loadingPlaceholder' | tr }} ...</span>
|
||||
<span ng-switch-default>
|
||||
<span ng-switch on="domain.inbound">
|
||||
<span ng-switch-when="true">
|
||||
<span ng-show="domain.loadingUsage">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount } }} {{ 'main.loadingPlaceholder' | tr }} ... </span>
|
||||
<span ng-show="!domain.loadingUsage">{{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount, usage: (domain.usage | prettyDecimalSize) } }}</span>
|
||||
</span>
|
||||
<span ng-switch-default>
|
||||
<span ng-show="domain.outbound">{{ 'emails.domains.outbound' | tr }}</span>
|
||||
<span ng-show="!domain.outbound">{{ 'emails.domains.disabled' | tr }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right no-wrap">
|
||||
<button class="btn btn-xs btn-default" ng-click="testEmail.show(domain)" uib-tooltip="{{ 'emails.domains.testEmailTooltip' | tr }}"><i class="fa fa-paper-plane"></i></button>
|
||||
<a href="/#/email/{{ domain.domain }}" class="btn btn-xs btn-default"><i class="fa fa-pencil-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mailbox sharing -->
|
||||
<h3 class="section-header" ng-show="user.isAtLeastAdmin">{{ 'emails.mailboxSharing.title' | tr }}</h3>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastAdmin" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>{{ 'emails.mailboxSharing.description' | tr }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2" style="padding-top: 12px;">
|
||||
<i class="fa fa-circle" ng-class="{ 'status-active': mailboxSharing.enabled, 'status-inactive': !mailboxSharing.enabled }"></i> {{ mailboxSharing.enabled ? 'main.statusEnabled' : 'main.statusDisabled' | tr }}
|
||||
</div>
|
||||
<div class="col-md-10 text-right">
|
||||
<button class="btn" ng-class="{ 'btn-danger': mailboxSharing.enabled, 'btn-primary': !mailboxSharing.enabled }" ng-click="mailboxSharing.submit()" ng-disabled="mailboxSharing.enable === mailboxSharing.enabled"><i class="fa fa-circle-notch fa-spin" ng-show="mailboxSharing.busy"></i> {{ mailboxSharing.enabled ? ('main.disableAction' | tr) : ('main.enableAction' | tr) }} </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- server location -->
|
||||
<h3 class="section-header" ng-show="user.isAtLeastAdmin">
|
||||
{{ 'emails.settings.location' | tr }}
|
||||
<div class="btn-group btn-group-sm pull-right">
|
||||
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="mailLocation.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'main.action.showLogs' | tr }}">
|
||||
<i class="fas fa-align-left"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="task in mailLocation.tasks">
|
||||
<a ng-href="/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
|
||||
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastAdmin">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<p ng-bind-html="'emails.changeDomainDialog.description' | tr"></p>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="mailLocation.subdomain" id="mailLocationLocationInput" name="location" placeholder="{{ 'emails.changeDomainDialog.locationPlaceholder' | tr }}" autofocus>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span>{{ (!mailLocation.subdomain ? '' : '.') + mailLocation.domain.domain }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li ng-repeat="domain in domains">
|
||||
<a href="" ng-click="mailLocation.domain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 10px;">
|
||||
<div ng-show="mailLocation.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailLocation.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-warning text-bold" ng-show="mailLocation.domain.provider === 'manual'" ng-bind-html="'emails.changeDomainDialog.manualInfo' | tr:{ domain: mailLocation.domain.domain }"></p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p ng-show="mailLocation.busy">{{ mailLocation.message }}</p>
|
||||
<p ng-hide="mailLocation.busy">
|
||||
<div class="has-error" ng-show="!mailLocation.active">{{ mailLocation.errorMessage }}</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<!-- save is always enabled so that user can "redo" the task -->
|
||||
<button class="btn btn-outline btn-primary" ng-click="mailLocation.change()" ng-hide="mailLocation.busy">{{ 'main.dialog.save' | tr }}</button>
|
||||
<button class="btn btn-outline btn-danger" ng-click="mailLocation.stop()" ng-show="mailLocation.busy" style="margin-right: 10px">{{ 'main.dialog.cancel' | tr }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- settings -->
|
||||
<h3 class="section-header" ng-show="user.isAtLeastAdmin">{{ 'emails.settings.title' | tr }}</h3>
|
||||
|
||||
<div class="card" ng-show="user.isAtLeastAdmin" style="margin-bottom: 15px;">
|
||||
<p ng-bind-html=" 'emails.settings.info' | tr "></p>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.maxMailSize' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ maxEmailSize.currentSize | prettyDecimalSize }} <a href="" ng-click="maxEmailSize.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.virtualAllMail' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ virtualAllMail.enabled ? 'main.statusEnabled' : 'main.statusDisabled' | tr }} <a href="" ng-click="virtualAllMail.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.acl' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'emails.settings.aclOverview' | tr:{ dnsblZonesCount: acl.dnsblZonesCount } }} <a href="" ng-click="acl.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.spamFilter' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span>{{ 'emails.settings.spamFilterOverview' | tr:{ blacklistCount: spamConfig.acl.blocklist.length } }} <a href="" ng-click="spamConfig.show()"><i class="fa fa-edit text-small"></i></a></span>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">{{ 'emails.settings.solrFts' | tr }}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right" ng-hide="ftsConfig.currentConfig">
|
||||
<i class="fa fa-circle-notch fa-spin"></i>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right" ng-show="ftsConfig.currentConfig">
|
||||
<span ng-show="ftsConfig.currentConfig.enabled">
|
||||
{{ 'emails.settings.solrEnabled' | tr }}
|
||||
<span ng-show="ftsConfig.running">/ {{ 'emails.settings.solrRunning' | tr }}</span>
|
||||
<span ng-hide="ftsConfig.running">/ {{ 'emails.settings.solrNotRunning' | tr }}</span>
|
||||
</span>
|
||||
<span ng-hide="ftsConfig.currentConfig.enabled">{{ 'emails.settings.solrDisabled' | tr }}</span>
|
||||
<a href="" ng-click="ftsConfig.show()"><i class="fa fa-edit text-small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||