Compare commits
924 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6cd06c65f | |||
| c942066878 | |||
| 0e742c7a8f | |||
| d2fce5eee1 | |||
| 5c510fd7b0 | |||
| e1ce85698c | |||
| 4b22608bc5 | |||
| 3a39266121 | |||
| 5d46ea73db | |||
| d51f4a7a23 | |||
| 79a81710f0 | |||
| c5a631da6f | |||
| 33672ab0aa | |||
| 311b3c77c8 | |||
| 17ca32c537 | |||
| 9d3f0bddde | |||
| 7f198bad92 | |||
| a912ada890 | |||
| 317113a1b3 | |||
| 564cb0b16f | |||
| ab0810f710 | |||
| 7a13ae0ac8 | |||
| c8e1678e55 | |||
| f49d276f5a | |||
| 05112d6a35 | |||
| fec49ce6f8 | |||
| 31a7faeef9 | |||
| dae7d046c4 | |||
| d428a6ce7a | |||
| ff25db3fea | |||
| 2baa87daeb | |||
| 42e51571a8 | |||
| acf0f649e8 | |||
| 64b9bfc392 | |||
| 744145be06 | |||
| 4a0dcd2899 | |||
| 92df726f34 | |||
| dbd5812c45 | |||
| 884f000058 | |||
| e936c431a2 | |||
| b01fa4e4ba | |||
| 96dcdc8e03 | |||
| 96d2efb3f0 | |||
| 37199d84cb | |||
| 52ee5f1d8a | |||
| 870a5e2bf6 | |||
| 10dd42b63f | |||
| 0999c71578 | |||
| e559a79506 | |||
| 1747ed84a8 | |||
| 22c6452227 | |||
| ad9b94d407 | |||
| 9f13bcd991 | |||
| 5bfa61a218 | |||
| 2cd03d5fec | |||
| c7fb648387 | |||
| 331b966556 | |||
| d28de50ec6 | |||
| 1ec8d5bbf2 | |||
| 4374d19ac2 | |||
| 071f1c9786 | |||
| d1425ca9ca | |||
| 2fd600d47d | |||
| 0eba778158 | |||
| b343152f14 | |||
| 31097e42a9 | |||
| 319b0af305 | |||
| f105c2f8f0 | |||
| 7cc201f423 | |||
| a23a99cc8d | |||
| be6b23c47b | |||
| f8333aab10 | |||
| d279c0a8dd | |||
| bd8867dba6 | |||
| 3d07328f47 | |||
| 0b0f6b8d8e | |||
| 4603b1fb14 | |||
| c8ef031e7d | |||
| 9877656eea | |||
| a64627e7d6 | |||
| 3b485dc1fe | |||
| e71c050724 | |||
| d54a9f6279 | |||
| f917e4a721 | |||
| 61fb10a951 | |||
| d3704a0f09 | |||
| c584929db5 | |||
| ea4cf20746 | |||
| 2056a2d783 | |||
| b81ab0ca15 | |||
| 70ac2d2bea | |||
| 07f3dabff6 | |||
| 72e43e4b7a | |||
| 0519bf86b3 | |||
| 6ba2f1f3e2 | |||
| 8613e92b31 | |||
| f79305f0a6 | |||
| 0f6aa66b21 | |||
| 51540c89d1 | |||
| a5227abd40 | |||
| 014b5aa671 | |||
| 10b85ea135 | |||
| f6e94e89c9 | |||
| 8a68fafedf | |||
| 9fbbd00dc5 | |||
| a8a5d3c3d6 | |||
| beb3bf9d1d | |||
| 1a2590e2c3 | |||
| 8858ef08c2 | |||
| 4520e16048 | |||
| 56cd29429e | |||
| 07aeaf8c8f | |||
| a2c7e5f29a | |||
| b3130d9441 | |||
| 0dd907e314 | |||
| 12a39064dc | |||
| b356af8eb4 | |||
| ae64094feb | |||
| 472cc3d7f2 | |||
| dbaed6094b | |||
| 8cb8f27e00 | |||
| d9ef81729f | |||
| d592d1c235 | |||
| e185301277 | |||
| 249a950dc1 | |||
| 540a4e5117 | |||
| 3d5da92c74 | |||
| 88fe141b16 | |||
| 87d2ffdb71 | |||
| 13cb3a1042 | |||
| 6be271565e | |||
| 8482171bf2 | |||
| c277893145 | |||
| 9a0290d7a6 | |||
| 777219ab87 | |||
| 16e06af76f | |||
| 3b776ec1bd | |||
| 175ed24a66 | |||
| 5f9b51e4db | |||
| ae9e35e091 | |||
| b60832a270 | |||
| 5186b14905 | |||
| 2a67a1e961 | |||
| 4d0d2ff099 | |||
| 37b8a382a8 | |||
| f06273aa77 | |||
| 600d1b7d38 | |||
| 9e74fa9578 | |||
| 282655dca8 | |||
| 3cc1d10b87 | |||
| 4bf8f8b3d3 | |||
| 2d81d07c0a | |||
| b0ac6e4e55 | |||
| 3db1fd0dde | |||
| df0eae256c | |||
| 9c348f79d7 | |||
| 525950a4da | |||
| 394af3ba0a | |||
| 27177acff7 | |||
| 864d68b6ac | |||
| c2e6c68f50 | |||
| b44fb5c45a | |||
| abaab18eb7 | |||
| d02205fba3 | |||
| c461a4daed | |||
| 4f85fa6ba9 | |||
| 4eea21c4f5 | |||
| 7816400c01 | |||
| 103841c364 | |||
| 54ac07f6fb | |||
| d9fc365aeb | |||
| 79d5b6ed40 | |||
| d4c8ae6cb7 | |||
| 6c37c9e786 | |||
| ff1c3dece3 | |||
| 449c1d3f4d | |||
| ae7d576967 | |||
| 3b3c878d0e | |||
| 057edb5488 | |||
| 4191b7314f | |||
| baf4141773 | |||
| 7a7281f2fa | |||
| 8e16908fce | |||
| 5e0a0456c4 | |||
| fb56001a52 | |||
| ecff97419b | |||
| ad135c5e32 | |||
| 522a2da114 | |||
| 7ca6a65559 | |||
| 309e891594 | |||
| 9623c2e6d2 | |||
| 3b1eff1702 | |||
| a00ff3df46 | |||
| 2f5d4dad9a | |||
| 20ee4c3567 | |||
| accffe8ef6 | |||
| e42ebb49ae | |||
| 1c49b69332 | |||
| 229006c51d | |||
| 15b6f5f6fb | |||
| ec7ac44b87 | |||
| 7b41f558d4 | |||
| 2bec513d2c | |||
| f6bf080c9e | |||
| 734a3d75db | |||
| e67b893ae4 | |||
| f9c53ca2c9 | |||
| 21f9c7cf6d | |||
| 43283b7218 | |||
| c2bb07ff3d | |||
| 8cd7f68c30 | |||
| 3e889d8c08 | |||
| a01395aed3 | |||
| 7b3459abff | |||
| 9a88f798fc | |||
| 256fd1cce2 | |||
| a8cb332232 | |||
| 89ea2b23a2 | |||
| 9b214a66c8 | |||
| 4c4689ec8d | |||
| c4e561dee6 | |||
| 69bfe5b750 | |||
| 137b3dc2ea | |||
| ea5f879181 | |||
| 25d5f34f68 | |||
| c8a6df77ac | |||
| 7b6ac46b6a | |||
| d77c648d54 | |||
| c3f47c5fa1 | |||
| f37a51b46f | |||
| 9feb3f35c3 | |||
| 5b44cf03ce | |||
| 86a6382463 | |||
| df991a3829 | |||
| f243c86b50 | |||
| b77be0a5e9 | |||
| 83be587c1a | |||
| c6048aea80 | |||
| 13b495b0f6 | |||
| ac64c35efa | |||
| 127b58601f | |||
| 1f795b45e9 | |||
| d26e35ee9a | |||
| 70e69e9dac | |||
| 3d88f4212d | |||
| ad90a9c8f3 | |||
| b43176764b | |||
| 77f24282e0 | |||
| 533f1f1102 | |||
| 92c5d8bd44 | |||
| d754ecb486 | |||
| b18987a95c | |||
| 0daec61f31 | |||
| 8ecf301eb8 | |||
| 6080646e89 | |||
| 970a521b66 | |||
| 9b5a92f942 | |||
| c4e0f13d67 | |||
| b6821746d0 | |||
| b98fc91a99 | |||
| bd7f7b867e | |||
| 32a3fea8d7 | |||
| 3561beab1d | |||
| 1d1fd52cae | |||
| c4ba438150 | |||
| 37f0ee08b1 | |||
| 240b02c94e | |||
| e8142ff3df | |||
| 832b363da7 | |||
| cf8f1ecd67 | |||
| ee5129830c | |||
| 9facfaffa8 | |||
| 31b859ba1a | |||
| b0f5a24c30 | |||
| b60b594e44 | |||
| a8cee1016b | |||
| b551af01fa | |||
| b178869a8e | |||
| 8e1c60d5f0 | |||
| 71ea49439e | |||
| 9d0a2f6535 | |||
| c091fffe90 | |||
| 7e05d05f94 | |||
| bd5b51c62f | |||
| 1d5eb2eced | |||
| 5073ead8fe | |||
| f61bcb621b | |||
| 42b695854b | |||
| 496ae8b1b2 | |||
| 4215a2592f | |||
| bca62033a1 | |||
| 18a91f9223 | |||
| 474d7c7a26 | |||
| 3f9a51ff46 | |||
| df3b5c41f9 | |||
| 8800d11ab0 | |||
| f021834ceb | |||
| f54a39a549 | |||
| c312ff3faf | |||
| db833594f4 | |||
| d36cf59829 | |||
| caae751698 | |||
| f7836eedce | |||
| 3ddc1a6229 | |||
| cf9869b71e | |||
| d31d8f574e | |||
| c1336428dc | |||
| f0e513bf0d | |||
| ee9e41c5a7 | |||
| 30f1257983 | |||
| f007cef208 | |||
| 19b5852098 | |||
| 2ec05ea7cc | |||
| e8dd8bff44 | |||
| ab3f65edfa | |||
| fcaf62ecf7 | |||
| 7cfa5a57f5 | |||
| 56d6c22d53 | |||
| 10b0b1e96e | |||
| 6ef1e9c86c | |||
| a0d0f2651b | |||
| b6edbad744 | |||
| 05748e23b1 | |||
| 12d87be106 | |||
| dfb83cc1f7 | |||
| 1840ae80c2 | |||
| 2e9d1d83a4 | |||
| 1ef83a3089 | |||
| b95c06a5dc | |||
| 8f789841a6 | |||
| 0f57a4dfd7 | |||
| 125c277c88 | |||
| a7674d436d | |||
| 48ba4cdf42 | |||
| 8a9265eb79 | |||
| 215dc6ea6d | |||
| 428dcb2348 | |||
| 1c79d14260 | |||
| a82b487b92 | |||
| 068369fca2 | |||
| d524143a53 | |||
| 05ebfa68c9 | |||
| 57d8fd7e10 | |||
| 64c556a337 | |||
| de268d1168 | |||
| 8fd09bcad4 | |||
| 8cc7d46d7b | |||
| bb56815210 | |||
| 332f2ab89f | |||
| d3780244cd | |||
| f7bfbd5080 | |||
| eef2ee1baa | |||
| 8d8d7d0f8c | |||
| 979a3d8f1f | |||
| 95b8f54b27 | |||
| 5f1d411b28 | |||
| 6e3fcf363a | |||
| 8e129c561d | |||
| fbc3ca4517 | |||
| 1668dfcce6 | |||
| da2a58a88b | |||
| 8390a868ba | |||
| 296c498464 | |||
| a92ff89221 | |||
| e3b2f41a1c | |||
| 9b3424fa90 | |||
| 678d947f16 | |||
| 0cd1568c64 | |||
| 4e25dbf5e3 | |||
| 14e10c038c | |||
| 2bc5be2105 | |||
| 36b8de470a | |||
| 9abe3dd7b3 | |||
| 96c98a6987 | |||
| e586145f50 | |||
| 611fbfa6df | |||
| 54eb4f78b2 | |||
| 1ce7bb8c02 | |||
| ef17e86465 | |||
| cd3e0492f5 | |||
| c3f98e327c | |||
| 20fc34a6fd | |||
| 87af67c36c | |||
| f33335a768 | |||
| a9fffd44d2 | |||
| e81b7e2e61 | |||
| 7fad15524f | |||
| 8367efa174 | |||
| aa86b7efff | |||
| b490450107 | |||
| a4a7ff46c5 | |||
| 418eeb7317 | |||
| 5b0fcd77e5 | |||
| 757a73ca6b | |||
| faab29cab7 | |||
| d1d6f12f05 | |||
| a979e8430f | |||
| f5f045e8f1 | |||
| 90e291ac8a | |||
| b49e1b3a97 | |||
| 5245feb739 | |||
| 550c8fb899 | |||
| 89ffc5d246 | |||
| 3330873ce0 | |||
| ff998a8bd1 | |||
| a1517f82bc | |||
| d8885b089a | |||
| beba06b8ba | |||
| 762be492bb | |||
| c00efff554 | |||
| 5258ae4b34 | |||
| a8b19e73fe | |||
| 92944c7adc | |||
| 0042173ddc | |||
| a438b5ba17 | |||
| b86643c052 | |||
| e897182b23 | |||
| 56329f02b6 | |||
| 11ab579de9 | |||
| 9aa1de11af | |||
| 939b395dcd | |||
| 118e9bf50b | |||
| 67828365ca | |||
| e0d99257fe | |||
| e628a98cb8 | |||
| a4f9284970 | |||
| c403a716dd | |||
| 4278c8bacb | |||
| 56bfbeca58 | |||
| b6f9eae7b5 | |||
| cad9250cb7 | |||
| 4ac8095554 | |||
| 05d16983eb | |||
| 12f821fd6b | |||
| 13c8bf5f19 | |||
| 9a44e30e0f | |||
| bf77572041 | |||
| 18ba4851d1 | |||
| 6620034d98 | |||
| e93ecc7e4a | |||
| f85085de41 | |||
| 310943590b | |||
| c1dcd52a28 | |||
| 9bee5e449f | |||
| 02e99066ca | |||
| 7761cf6dbe | |||
| 7715d4bdda | |||
| f3c4906f88 | |||
| 3dc16fa07b | |||
| be58bdf24f | |||
| ac170c67bf | |||
| 169cd9d743 | |||
| a1931f08a8 | |||
| 5617206c6d | |||
| cee00b8b2b | |||
| 323a8acbd5 | |||
| d488b10848 | |||
| 24c99364ef | |||
| f8b6975e70 | |||
| c416e8c1fb | |||
| 51626dfd04 | |||
| cb05e58f1e | |||
| 24c8cfd402 | |||
| 8257b0b582 | |||
| e612f2c281 | |||
| 155df59b1d | |||
| 6efab9f386 | |||
| 422105a507 | |||
| d58438ab1d | |||
| 209aa1a89f | |||
| 07ac81583e | |||
| ee6249bb17 | |||
| b8ca3f8604 | |||
| 85b5469d96 | |||
| d3ddf7b191 | |||
| ebf196ec73 | |||
| 85c3833452 | |||
| 8fb4735fc2 | |||
| 6bb09f10d2 | |||
| a4ff67e8f4 | |||
| b9a1756f90 | |||
| 72ff8e67ed | |||
| d7006365eb | |||
| 86ef78b296 | |||
| 0d84e409c5 | |||
| 17c7b33167 | |||
| 856e1613a0 | |||
| 8db34289a7 | |||
| 3016f70349 | |||
| bdf212fdb3 | |||
| c6216f54f8 | |||
| 945c747e3e | |||
| 552fca8df7 | |||
| 4dac160619 | |||
| fc0e746e1b | |||
| 8374b5fc0c | |||
| 52923ed35b | |||
| f002ccfa3a | |||
| b64cc44531 | |||
| 1b51982551 | |||
| f4bbf6eb73 | |||
| 10c94efa57 | |||
| c84316e974 | |||
| 7e4d1fbf49 | |||
| 31c7c8b46f | |||
| 8cdb6646c4 | |||
| 43a51e5f2f | |||
| d0b54ea44b | |||
| c8fce82dc1 | |||
| 3ffccaa7e5 | |||
| 3f7e7a1474 | |||
| d30c15464b | |||
| 2a1f748469 | |||
| 74b429b1bf | |||
| 7afe1b517c | |||
| fcdcf5692f | |||
| 735baa67ed | |||
| ac00caf83e | |||
| 2ccda353e9 | |||
| 4257ca54b7 | |||
| 20dd2ef98d | |||
| 8be52c9ddd | |||
| 3bea6619fa | |||
| 0e5fb11c52 | |||
| fc47f7701d | |||
| 3e4ccf8de4 | |||
| 8abb3c709b | |||
| 5263900620 | |||
| 54ee9e5072 | |||
| 20f4857405 | |||
| 55110dd39d | |||
| 07d8111df9 | |||
| a701b97f1e | |||
| b025537d62 | |||
| e68bc2de65 | |||
| d0b1c93382 | |||
| a08fa9eabf | |||
| 65ef130bda | |||
| 32471a71e5 | |||
| ca80f01907 | |||
| b1fae57922 | |||
| dea01dda2d | |||
| 9777619259 | |||
| 6db0f84f8f | |||
| 84e382fcc5 | |||
| eceed1b934 | |||
| e7b5cd74a4 | |||
| a98930791c | |||
| 67b0389097 | |||
| ff30595fb4 | |||
| d272b62c45 | |||
| c1973023d0 | |||
| ef4689619e | |||
| 9ed297a49d | |||
| 9950729080 | |||
| 36eec9d061 | |||
| fd3a977009 | |||
| ff88832cca | |||
| d69b72b77d | |||
| d9bd9e996b | |||
| f45580be70 | |||
| 0fbeb11304 | |||
| 6a70c13b57 | |||
| f797520e56 | |||
| d060db3c96 | |||
| 298344c2ab | |||
| 53dbeadabb | |||
| 93087d7aa7 | |||
| c304517fc2 | |||
| af13e034c3 | |||
| e6b9d59160 | |||
| bd9cbe30c5 | |||
| f627226da9 | |||
| bab3272970 | |||
| 003085db7e | |||
| 8f98bd37c7 | |||
| 6971ef1ede | |||
| 0baac3dc49 | |||
| 16feb8602d | |||
| d623129d56 | |||
| 9cc4c33bb1 | |||
| c087b40d4a | |||
| a82e074bd4 | |||
| 3365c099bb | |||
| d8705de26f | |||
| 3af181e736 | |||
| ba6c01b7c6 | |||
| 7a7cdcb041 | |||
| 09e52bf199 | |||
| a8c39a1587 | |||
| 68536d6c34 | |||
| f57c27e755 | |||
| 9a97a27c8a | |||
| 07bb90dda9 | |||
| 3bb156f4da | |||
| e13e0efcc6 | |||
| 3ae0a94159 | |||
| eec67ec7dc | |||
| cf51853eec | |||
| 67838b28a4 | |||
| bf68e3b7d5 | |||
| 91ed0bb8bd | |||
| 55fe791c2a | |||
| 747efac2ec | |||
| a87df2e9f6 | |||
| 2e4a664744 | |||
| 579a22ea45 | |||
| abff850427 | |||
| f1154058ba | |||
| cf9f308b7f | |||
| 1791df236c | |||
| 7211f94f08 | |||
| 7b0343c87f | |||
| b80f0276b4 | |||
| 8f66fcbb00 | |||
| fe449ee1f3 | |||
| 34d6d95186 | |||
| 05ddc0660a | |||
| c6047a8428 | |||
| bc4838578e | |||
| 548996405a | |||
| a9a5f0bd14 | |||
| ec05ff6123 | |||
| 10c7ab421b | |||
| a8a5cc53ea | |||
| 8fe48ca438 | |||
| cbd5a515a9 | |||
| 5912adff93 | |||
| 983bc21448 | |||
| 6d08368462 | |||
| cde7bb3524 | |||
| 133dc91561 | |||
| f408f59beb | |||
| 8763bf11ab | |||
| 99592a52be | |||
| 25a8a52573 | |||
| 5901344459 | |||
| b07ae4bc42 | |||
| 0d9e61480a | |||
| baaea96b1d | |||
| e156cb5c2e | |||
| ff77b2b3dc | |||
| 4dc225184d | |||
| 49a0266224 | |||
| 7a62d96b91 | |||
| 641454fa14 | |||
| cf2f0ec936 | |||
| 76375c9471 | |||
| 0894c56c19 | |||
| 7de89ffe57 | |||
| e0c01343a8 | |||
| 48ed4f971c | |||
| 9e8c8a019e | |||
| 96f4ca2cc9 | |||
| fad59604f9 | |||
| ac0e8da0ba | |||
| fc5c3fd73e | |||
| 536fbb23a1 | |||
| ac01006398 | |||
| ade8654cc4 | |||
| 8e03f26fb5 | |||
| dab51bef7b | |||
| bea7fb5fe5 | |||
| d6b3240506 | |||
| 19e53d8bc8 | |||
| 5923d72803 | |||
| 8cde976358 | |||
| 256a5b50ac | |||
| 8b236f68f4 | |||
| c18f6d3978 | |||
| e65733754c | |||
| 4505630896 | |||
| 044804143f | |||
| 1e9d7c9b93 | |||
| 573fe3cad3 | |||
| cd3880c85c | |||
| 02cd8ed7ae | |||
| 3d69f96b96 | |||
| 963a15abe7 | |||
| 40542f0461 | |||
| bfc2af71a4 | |||
| 35142bb61d | |||
| 39626e0df9 | |||
| ca40730600 | |||
| 4158e196d6 | |||
| 1516e800dd | |||
| 0b96f69a1d | |||
| bb418e51d4 | |||
| 5fb23ab8bf | |||
| 117b01c48b | |||
| 18719815a3 | |||
| f795595107 | |||
| be3f9465eb | |||
| c598daab9b | |||
| 438b25672f | |||
| 1e12ddd8e2 | |||
| dccf55d57a | |||
| 3397845ccc | |||
| 82ae13ba3e | |||
| 8c830761f3 | |||
| ad5c134887 | |||
| 150bf124a9 | |||
| 1c96c0ccbf | |||
| cf8ad3d697 | |||
| 5b723ec954 | |||
| 633c708c33 | |||
| e4784108f7 | |||
| b4103a4edb | |||
| cfdd32708a | |||
| 8e97c1e9e8 | |||
| e009bebfaf | |||
| d3d190989d | |||
| 1b36b52019 | |||
| e526e96e2d | |||
| 1659d74938 | |||
| f7161dc39b | |||
| d8ea41e4dd | |||
| fe0a03db2f | |||
| 49afed7751 | |||
| a788b1706b | |||
| d57fd20ca2 | |||
| 5ecc1b09c8 | |||
| f1dfc9666b | |||
| b503584431 | |||
| 0dfe1c8e3a | |||
| c5630d90ec | |||
| f8973a3cf7 | |||
| 228be8376f | |||
| 3fee3b0224 | |||
| b4e705b347 | |||
| 51e59bebfc | |||
| a56ac31f4b | |||
| af05f081d2 | |||
| b97d79d261 | |||
| d227e6339f | |||
| 74c503fea0 | |||
| ed8b0c4d3c | |||
| cbb17e92f0 | |||
| f9d64cf777 | |||
| 7fdd6d19cf | |||
| 60ca252b63 | |||
| 2027837c18 | |||
| 67904568b1 | |||
| afbc559a47 | |||
| 67c312642c | |||
| 89eb007e43 | |||
| 88c3f3204b | |||
| 052ab98cd3 | |||
| cd01c0e31b | |||
| c3d3228f65 | |||
| f04647192f | |||
| c00f0c9f52 | |||
| 690e04c377 | |||
| 545880dcce | |||
| fedf5d2203 | |||
| 5242e55826 | |||
| 1bf86e73a7 | |||
| c3fe72f561 | |||
| b54555a4f7 | |||
| 4c080360a7 | |||
| fdea15eef4 | |||
| 5a4dd97abe | |||
| e47e3d936b | |||
| 0e9fd4373e | |||
| 69a10c754a | |||
| a8ba965c0d | |||
| 847aed3519 | |||
| fed9345bea | |||
| d71b6d0ab0 | |||
| 40e5521fbf | |||
| c7781d2e75 | |||
| 0ccf67000c | |||
| f4e81aca9d | |||
| 2d654cf759 | |||
| 0b728162ae | |||
| f52b426652 | |||
| ad0528d763 | |||
| 263adcf2b5 | |||
| b201e274f1 | |||
| c1a6a73b7d | |||
| ed4764cc70 | |||
| 8d84b3adbc | |||
| 35db674c88 | |||
| 3e1850a713 | |||
| 8c625c0ecd | |||
| bf1de8e7a4 | |||
| 8c2dca3770 | |||
| 23adbddfb7 | |||
| b1b6b64e68 | |||
| 0c7909df09 | |||
| 641bfd8c06 | |||
| 52140c3208 | |||
| 1baa9173cb | |||
| 26749c2116 | |||
| a1ecb9ad39 | |||
| b398fde24e | |||
| a5767b60fb | |||
| ef196c384c | |||
| 920def7446 | |||
| bec98dcaa2 | |||
| 747be07322 | |||
| c1fc6f0bce | |||
| 851c42e630 | |||
| 4e5a4a9c17 | |||
| df19d500de | |||
| 4fd4c9802e | |||
| 23d0eeab16 | |||
| b66557f3be | |||
| 580586cab3 | |||
| 9a97ef1dad | |||
| b4f7503d85 | |||
| b89406a89a | |||
| 932cdae22e | |||
| 34ef86cfff | |||
| 023bace2a9 | |||
| 467b63cffb | |||
| 49ffb0fa68 | |||
| 04984dea98 | |||
| 78e9a768b4 | |||
| 401f877388 | |||
| b3ba1aa3b9 | |||
| ae76aebe24 | |||
| b5e1bf2867 | |||
| fb3a0da54f | |||
| 7d197f405d | |||
| bc7af3a68b | |||
| 120e79229a | |||
| f0e4bbcda9 | |||
| 57e31c1dfb | |||
| 8f2f414f5e | |||
| 879f91693e | |||
| 0c504b52e4 | |||
| bbaa5f9fa8 | |||
| 2a9f7fde28 | |||
| 0ec7c87851 | |||
| cf515abc4c | |||
| 99cab08ab7 | |||
| e60d40ee9c | |||
| c90fa68f16 | |||
| 61ae86e927 | |||
| 86c995906c | |||
| a77b88aaf6 | |||
| 0d288f3206 | |||
| d9d6244931 | |||
| 4d984f0524 | |||
| b456b0143b | |||
| 3ebcfd5598 | |||
| 745ea3509e | |||
| 8f5d4e5bc2 | |||
| acd7d3bbac | |||
| f3bd6598e7 | |||
| ce48fdc445 | |||
| 5aea223c14 | |||
| 1f59d3179c | |||
| 1f6e1db695 | |||
| 5b46c372ab | |||
| f143d3e1c2 | |||
| c7639309ef | |||
| b788531e47 | |||
| 00fa2ca804 | |||
| 74cdf09ebc | |||
| 80ec12740a | |||
| a0002ff97b | |||
| 2cb0db02e3 | |||
| bed8d71f7e | |||
| 668b385a10 | |||
| d96df469a4 | |||
| 8edbe1aa2c | |||
| 99ece9a61a | |||
| 4b5253dd6b | |||
| 5e10294a16 | |||
| 226d2069d9 | |||
| 8d1365b712 | |||
| 45624d905e | |||
| a5d9c931ca | |||
| f0db2b2f6e | |||
| cde5913ace | |||
| ceea9fc501 | |||
| eb678b4533 | |||
| dcbe8bb3dc | |||
| 555310a4ca | |||
| 3a57358f58 | |||
| 9982ba2fec | |||
| 11b652f851 | |||
| 380950a615 | |||
| e8b07738a5 | |||
| 9d1163e73e | |||
| e2f0c73bab | |||
| 183873afff | |||
| b356475741 | |||
| 279bae115a | |||
| 82bb5a96ad | |||
| a4e9ffcc99 | |||
| 1c25427c5c | |||
| 2ec22eb6cd | |||
| 2ff508607d | |||
| 69a34c65f7 | |||
| f34c1520a4 | |||
| f3fa8eec50 | |||
| 9baa9b1dd6 | |||
| c1c1af1ded | |||
| 4a8c1b7de4 | |||
| 1f35165919 | |||
| 2e85508426 | |||
| 631b218057 | |||
| e8cfc88d83 | |||
| 78502a0cd0 | |||
| 2bcb6a146b | |||
| ae474b5e5b | |||
| babed18af0 | |||
| 3fa7e3e36d | |||
| 45587d5f15 | |||
| e98f6997ca | |||
| 21d1b71653 | |||
| afd7b5650f | |||
| a0687d555a | |||
| f543953389 | |||
| 2c4c7586b7 | |||
| e92921a6cc | |||
| 0322bcd047 | |||
| 26c914be96 | |||
| 75863faf58 | |||
| 99b3ec4ce3 | |||
| d97ffd863b |
@@ -8,6 +8,9 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
[XamlTypeInfo.g.cs]
|
||||
dotnet_diagnostic.CS0612.severity = none
|
||||
dotnet_diagnostic.CS0618.severity = none
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
@@ -149,7 +152,7 @@ csharp_preferred_modifier_order = public,private,protected,internal,static,exter
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_namespace_declarations = file_scoped:error
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
@@ -288,3 +291,5 @@ csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
|
||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:silent
|
||||
csharp_prefer_system_threading_lock = true:suggestion
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# Copilot Instructions for Wino-Mail Project
|
||||
|
||||
## Project Overview
|
||||
|
||||
Wino Mail is a native Windows mail client targeting Windows 10 1809+ and Windows 11. The project is **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI, Wino.Core.WinUI), never edit the old Wino.Mail UWP project.
|
||||
|
||||
### Key Technologies
|
||||
- **WinUI 3** for UI (previously UWP/WinUI 2)
|
||||
- **MVVM Toolkit** (CommunityToolkit.Mvvm) for ViewModels with source generators
|
||||
- **Messenger** pattern (WeakReferenceMessenger.Default) for event pub-sub throughout the codebase
|
||||
- **SQLite** database stored in publisher cache folder (not local storage)
|
||||
- **WebView2** for mail rendering/composition with custom HTML/JavaScript editors
|
||||
- **MimeKit/MailKit** for IMAP/SMTP operations
|
||||
- **Microsoft Graph SDK** for Outlook synchronization
|
||||
- **Gmail API** for Gmail synchronization
|
||||
|
||||
### Solution Structure
|
||||
```
|
||||
Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts)
|
||||
Wino.Core → Synchronization engine, authenticators, request processing
|
||||
Wino.Services → Database, mail, folder, account services
|
||||
Wino.Mail.ViewModels → Mail-specific ViewModels
|
||||
Wino.Core.ViewModels → Shared ViewModels (settings, personalization)
|
||||
Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this)
|
||||
Wino.Mail → **Deprecated UWP project** (DO NOT EDIT)
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Mail Synchronization Flow
|
||||
1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move)
|
||||
2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers
|
||||
3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations
|
||||
4. **ChangeProcessors** (OutlookChangeProcessor, etc.) → Apply changes to local database
|
||||
5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.)
|
||||
|
||||
### Queue-Based Sync (New Pattern - See QUEUE_SYNC_IMPLEMENTATION.md)
|
||||
- Initial sync now queues mail IDs first (MailItemQueue table), downloads metadata only (no MIME)
|
||||
- MIME content downloaded on-demand when user opens mail
|
||||
- Synchronizers override `QueueMailIdsForInitialSyncAsync()`, `DownloadMailsFromQueueAsync()`, `CreateMinimalMailCopyAsync()`
|
||||
- Check `MailItemFolder.IsInitialSyncCompleted` to determine sync state
|
||||
|
||||
### Dependency Injection Setup
|
||||
Services registered in extension methods across projects:
|
||||
- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs
|
||||
- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs
|
||||
- `RegisterCoreUWPServices()` in CoreUWPContainerSetup.cs
|
||||
- ViewModels registered in App.xaml.cs with AddTransient/AddSingleton
|
||||
|
||||
### Messenger Pattern (Event Pub-Sub)
|
||||
- All ViewModels inherit from CoreBaseViewModel or MailBaseViewModel which implement IRecipient<T>
|
||||
- Register/unregister message handlers in `RegisterRecipients()` / `UnregisterRecipients()`
|
||||
- Send messages via `WeakReferenceMessenger.Default.Send(new MessageType(...))`
|
||||
- Common messages: MailAddedMessage, MailUpdatedMessage, NavigationRequested, ThemeChanged
|
||||
|
||||
## ViewModels Development Guidelines
|
||||
|
||||
### Observable Properties - Critical Pattern
|
||||
- **ALWAYS** use `public partial` observable properties with MVVM Toolkit source generators
|
||||
- **NEVER** use private fields with `[ObservableProperty]` attribute
|
||||
- **Correct:**
|
||||
```csharp
|
||||
[ObservableProperty]
|
||||
public partial string SearchQuery { get; set; } = string.Empty;
|
||||
```
|
||||
- **Incorrect:**
|
||||
```csharp
|
||||
[ObservableProperty]
|
||||
private string searchQuery = string.Empty; // WRONG - will not work
|
||||
```
|
||||
|
||||
### ViewModels Structure
|
||||
- Inherit from MailBaseViewModel (for mail features) or CoreBaseViewModel (for shared features)
|
||||
- Use `[RelayCommand]` for command methods - source generator creates Command properties
|
||||
- Implement IRecipient<TMessage> for message handlers
|
||||
- Use `IMailDialogService` for Mail-related dialogs, `IDialogServiceBase` for core dialogs
|
||||
- Call `RegisterRecipients()` in constructor/OnNavigatedTo, `UnregisterRecipients()` in OnNavigatedFrom
|
||||
|
||||
## Localization System
|
||||
|
||||
### Translation Workflow (Custom T4-based System)
|
||||
1. Add English strings ONLY to `Wino.Core.Domain/Translations/en_US/resources.json`
|
||||
2. Build the project - source generators automatically create Translator properties
|
||||
3. Use `Translator.{PropertyName}` in ViewModels, XAML (with x:Bind, OneTime mode)
|
||||
4. **NEVER** edit other language files - Crowdin manages translations automatically
|
||||
5. **NEVER** hardcode user-facing strings
|
||||
|
||||
### Usage Examples
|
||||
```csharp
|
||||
// ViewModel
|
||||
_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle, message);
|
||||
|
||||
// XAML
|
||||
<TextBlock Text="{x:Bind Translator.Settings_Title, Mode=OneTime}" />
|
||||
```
|
||||
|
||||
## UI Data Binding and Converters
|
||||
|
||||
### WinUI 3 Automatic Conversions
|
||||
- **NEVER** create IValueConverter classes or add them to Converters.xaml
|
||||
- **NEVER** use BoolToVisibilityConverter - WinUI 3 SDK automatically converts bool to Visibility
|
||||
- Direct binding: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
|
||||
|
||||
### XamlHelpers for Complex Conversions
|
||||
- **ALWAYS** use XamlHelpers static methods instead of converters
|
||||
- Add xmlns: `xmlns:helpers="using:Wino.Helpers"`
|
||||
- Usage: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(PropertyName), Mode=OneWay}`
|
||||
- Available methods: ReverseBoolToVisibilityConverter, CountToBooleanConverter, BoolToSelectionMode, Base64ToBitmapImage
|
||||
- Add new methods to XamlHelpers.cs when needed, don't create converters
|
||||
|
||||
## WebView2 Mail Rendering
|
||||
|
||||
### Architecture
|
||||
- **reader.html** (Wino.Mail.WinUI/JS/) for reading mails
|
||||
- **editor.html** for composing mails (uses Jodit editor, not Quill as originally planned)
|
||||
- WebView2 uses virtual host mapping: `https://wino.mail/reader.html`
|
||||
- JavaScript interop via `ExecuteScriptFunctionAsync()` to call functions like `RenderHTML()`
|
||||
- MIME content downloaded on-demand, not during sync
|
||||
|
||||
### Key Patterns
|
||||
- Set environment variables for WebView2 before initialization (overlay scrollbars, cache)
|
||||
- Wait for DOMContentLoaded event before script execution
|
||||
- Handle theme changes by updating editor CSS dynamically
|
||||
- Cancel external navigation, open in browser via Launcher.LaunchUriAsync()
|
||||
|
||||
## File Structure and Project Organization
|
||||
|
||||
### Critical Rules
|
||||
- **NEVER** edit files in Wino.Mail (UWP) project - it's deprecated
|
||||
- **ALWAYS** work with Wino.Mail.WinUI for UI components
|
||||
- Place ViewModels in Wino.Mail.ViewModels (mail-specific) or Wino.Core.ViewModels (shared)
|
||||
- Create abstract base classes in Views/Abstract folders
|
||||
- Mail-specific dialog services go in Wino.Mail.WinUI/Services
|
||||
|
||||
### Database and Storage
|
||||
- SQLite database in publisher cache folder (not app local storage)
|
||||
- EML files stored in app local storage, referenced by MailCopy.FileId
|
||||
- Paths resolved via MimeFileService.GetMimeMessagePath()
|
||||
- Database entities in Wino.Core.Domain/Entities
|
||||
|
||||
## Error Handling and User Feedback
|
||||
|
||||
### Exception Handling Patterns
|
||||
```csharp
|
||||
try {
|
||||
await operation();
|
||||
} catch (UnavailableSpecialFolderException ex) {
|
||||
_dialogService.InfoBarMessage(title, message, InfoBarMessageType.Warning, buttonText, action);
|
||||
} catch (NotImplementedException) {
|
||||
_dialogService.ShowNotSupportedMessage();
|
||||
}
|
||||
```
|
||||
|
||||
### Dialog Service Methods
|
||||
- `InfoBarMessage()` - simple notifications with optional action button
|
||||
- `ShowConfirmationDialogAsync()` - yes/no dialogs
|
||||
- `PickFilesAsync()` - file selection
|
||||
- Always check for null/empty results from dialog operations
|
||||
|
||||
## Code Style and Best Practices
|
||||
|
||||
- Use `var` where type is obvious from right side
|
||||
- String interpolation over string.Format for simple cases
|
||||
- Keep methods focused and single-responsibility
|
||||
- Add XML documentation for public APIs
|
||||
- Avoid introducing new NuGet packages - maximize use of existing libraries
|
||||
- Wrap async operations in try-catch blocks
|
||||
- Log errors via IWinoLogger but don't expose technical details to users
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Building and Running
|
||||
- Open WinoMail.slnx in Visual Studio 2022+
|
||||
- Target platforms: x86, x64, ARM64 (ARM32 being phased out)
|
||||
- Minimum: Windows 10 1809, Target: Windows 11 22H2
|
||||
- Set Wino.Mail.WinUI as startup project
|
||||
|
||||
### Testing
|
||||
- Test suite in Wino.Core.Tests
|
||||
- Manual testing required for UI/WebView2 interactions
|
||||
- Test synchronization with real accounts when modifying synchronizers
|
||||
|
||||
### Common Pitfalls
|
||||
- Forgetting to register ViewModels in App.xaml.cs RegisterViewModels()
|
||||
- Not calling RegisterRecipients() for message handlers
|
||||
- Using private fields with [ObservableProperty] (won't work - must be public partial)
|
||||
- Creating IValueConverter classes instead of using XamlHelpers
|
||||
- Editing UWP project files instead of WinUI equivalents
|
||||
- Hardcoding strings instead of using Translator
|
||||
- Forgetting to unregister Messenger recipients (memory leaks)
|
||||
@@ -0,0 +1,115 @@
|
||||
name: PR WinUI Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
build-winui:
|
||||
name: Build project (${{ matrix.platform }})
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: windows-latest
|
||||
continue-on-error: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: x86
|
||||
rid: win-x86
|
||||
- platform: x64
|
||||
rid: win-x64
|
||||
- platform: ARM64
|
||||
rid: win-arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Restore WinUI project dependencies
|
||||
run: dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=${{ matrix.platform }} -p:RuntimeIdentifier=${{ matrix.rid }}
|
||||
|
||||
- name: Build WinUI project
|
||||
run: dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configuration Release --no-restore -p:Platform=${{ matrix.platform }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:GenerateAppxPackageOnBuild=false -p:AppxPackageSigningEnabled=false
|
||||
|
||||
core-tests:
|
||||
name: Run Core tests
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: windows-latest
|
||||
continue-on-error: ${{ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Restore Core test projects
|
||||
shell: pwsh
|
||||
run: |
|
||||
$coreTests = Get-ChildItem -Path . -Recurse -Filter "*Core*.Tests.csproj" | ForEach-Object { $_.FullName }
|
||||
if (-not $coreTests) {
|
||||
throw "No Core test projects were found."
|
||||
}
|
||||
|
||||
foreach ($project in $coreTests) {
|
||||
dotnet restore $project --configfile nuget.config
|
||||
}
|
||||
|
||||
- name: Run Core test projects
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path TestResults -Force | Out-Null
|
||||
$coreTests = Get-ChildItem -Path . -Recurse -Filter "*Core*.Tests.csproj"
|
||||
if (-not $coreTests) {
|
||||
throw "No Core test projects were found."
|
||||
}
|
||||
|
||||
foreach ($project in $coreTests) {
|
||||
$name = $project.BaseName
|
||||
dotnet test $project.FullName --configuration Release --no-restore --verbosity normal --logger "trx;LogFileName=$name.trx" --results-directory TestResults
|
||||
}
|
||||
|
||||
- name: Upload Core test result artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-test-results
|
||||
path: TestResults/*.trx
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Publish Core test report
|
||||
if: always()
|
||||
uses: EnricoMi/publish-unit-test-result-action/windows@v2
|
||||
with:
|
||||
trx_files: TestResults/*.trx
|
||||
check_name: Core test results
|
||||
|
||||
enforce-for-non-maintainers:
|
||||
name: Enforce required checks (non-maintainers)
|
||||
if: github.event.pull_request.draft == false && !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-winui
|
||||
- core-tests
|
||||
|
||||
steps:
|
||||
- name: Fail when build or tests fail for non-maintainers
|
||||
if: needs.build-winui.result != 'success' || needs.core-tests.result != 'success'
|
||||
run: |
|
||||
echo "WinUI build and Core tests must pass for non-maintainer pull requests."
|
||||
exit 1
|
||||
|
||||
- name: Confirm build and test success for non-maintainers
|
||||
run: echo "WinUI build and Core tests passed."
|
||||
@@ -206,9 +206,6 @@ PublishScripts/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Nuget personal access tokens and Credentials
|
||||
nuget.config
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
@@ -402,3 +399,4 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml
|
||||
Wino/obj/x86/Debug/XamlSaveStateFile.xml
|
||||
*.cache
|
||||
.vs/Wino/v16/.suo
|
||||
/.claude/settings.local.json
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replacing the deprecated Windows Mail & Calendar. It's **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI), never edit the deprecated Wino.Mail UWP project.
|
||||
|
||||
## Build and Development Commands
|
||||
|
||||
```bash
|
||||
# Open solution
|
||||
# WinoMail.slnx is the main solution file (VS 2022+)
|
||||
|
||||
# Build from command line
|
||||
dotnet build WinoMail.slnx -c Debug
|
||||
|
||||
# Run tests
|
||||
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj
|
||||
|
||||
# Build specific platform
|
||||
dotnet build WinoMail.slnx -c Debug /p:Platform=x64
|
||||
```
|
||||
|
||||
**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
|
||||
|
||||
**Startup project:** Wino.Mail.WinUI
|
||||
|
||||
**Platforms:** x86, x64, ARM64
|
||||
|
||||
## Architecture
|
||||
|
||||
### Solution Structure
|
||||
```
|
||||
Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts)
|
||||
Wino.Core → Synchronization engine, authenticators, request processing
|
||||
Wino.Services → Database, mail, folder, account services
|
||||
Wino.Authentication → OAuth2 authenticators (Outlook, Gmail)
|
||||
Wino.Mail.ViewModels → Mail-specific ViewModels
|
||||
Wino.Core.ViewModels → Shared ViewModels (settings, personalization)
|
||||
Wino.Messaging → Pub-sub message definitions
|
||||
Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this)
|
||||
Wino.Mail → **Deprecated UWP project** (DO NOT EDIT)
|
||||
```
|
||||
|
||||
### Mail Synchronization Flow
|
||||
1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move)
|
||||
2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers
|
||||
3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations
|
||||
4. **ChangeProcessors** → Apply changes to local SQLite database
|
||||
5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.)
|
||||
|
||||
### Synchronizer Types
|
||||
- **OutlookSynchronizer** - Microsoft Graph SDK for Office 365
|
||||
- **GmailSynchronizer** - Gmail API
|
||||
- **ImapSynchronizer** - MimeKit/MailKit for IMAP/SMTP
|
||||
|
||||
### Queue-Based Sync Pattern
|
||||
- Initial sync queues mail IDs first (MailItemQueue table), downloads metadata only
|
||||
- MIME content downloaded on-demand when user opens mail
|
||||
- Check `MailItemFolder.IsInitialSyncCompleted` for sync state
|
||||
- See QUEUE_SYNC_IMPLEMENTATION.md for details
|
||||
|
||||
### Dependency Injection
|
||||
- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs
|
||||
- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs
|
||||
- ViewModels registered in App.xaml.cs
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### MVVM with Source Generators
|
||||
**CORRECT - use public partial properties:**
|
||||
```csharp
|
||||
[ObservableProperty]
|
||||
public partial string SearchQuery { get; set; } = string.Empty;
|
||||
```
|
||||
|
||||
**WRONG - will not work:**
|
||||
```csharp
|
||||
[ObservableProperty]
|
||||
private string searchQuery = string.Empty;
|
||||
```
|
||||
|
||||
### Messenger Pattern
|
||||
- ViewModels inherit from CoreBaseViewModel or MailBaseViewModel
|
||||
- Register handlers in `RegisterRecipients()`, unregister in `UnregisterRecipients()`
|
||||
- Send via `WeakReferenceMessenger.Default.Send(new MessageType(...))`
|
||||
|
||||
### Data Binding - No Converters
|
||||
- **NEVER** create IValueConverter classes
|
||||
- WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
|
||||
- Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}`
|
||||
|
||||
## Localization
|
||||
|
||||
1. Add English strings ONLY to `Wino.Core.Domain/Translations/en_US/resources.json`
|
||||
2. Build project - source generators create Translator properties
|
||||
3. Use `Translator.{PropertyName}` in code/XAML
|
||||
4. **NEVER** edit other language files - Crowdin manages translations
|
||||
|
||||
## Storage
|
||||
|
||||
- **SQLite database** in publisher cache folder (shared with future Wino Calendar)
|
||||
- **EML files** in app local storage, referenced by `MailCopy.FileId`
|
||||
- Paths resolved via `MimeFileService.GetMimeMessagePath()`
|
||||
|
||||
## WebView2 Mail Rendering
|
||||
|
||||
- `reader.html` for reading mails, `editor.html` for composing (Jodit editor)
|
||||
- Virtual host mapping: `https://wino.mail/reader.html`
|
||||
- JavaScript interop via `ExecuteScriptFunctionAsync()`
|
||||
- MIME content downloaded on-demand, not during sync
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting to register ViewModels in App.xaml.cs `RegisterViewModels()`
|
||||
- Not calling `RegisterRecipients()` for message handlers
|
||||
- Using private fields with `[ObservableProperty]` instead of public partial
|
||||
- Creating IValueConverter classes instead of using XamlHelpers
|
||||
- Editing UWP project files instead of WinUI equivalents
|
||||
- Hardcoding strings instead of using Translator
|
||||
- Forgetting to unregister Messenger recipients (memory leaks)
|
||||
|
||||
## Code Style
|
||||
|
||||
- Avoid introducing new NuGet packages when possible
|
||||
- Use existing libraries (MimeKit, MailKit, Microsoft Graph, Gmail API)
|
||||
- Use `var` where type is obvious
|
||||
- String interpolation over string.Format
|
||||
- Wrap async operations in try-catch
|
||||
- Log errors via IWinoLogger
|
||||
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
|
||||
@@ -15,7 +15,12 @@ Wino Mail is [Universal Windows Platform](https://learn.microsoft.com/en-us/wind
|
||||
**Min Version:** Windows 10 1809
|
||||
**Target Version:** Windows 11 22H2
|
||||
|
||||
It's pretty straightforward after cloning the repo. There are no prerequisites needed. Just open **Wino.sln** solution in your IDE and launch.
|
||||
## Prerequisites
|
||||
|
||||
* ".NET desktop development" workload in Visual Studio 2022+
|
||||
* .NET SDK 8.0+
|
||||
|
||||
With those installed, it's pretty straightforward after cloning the repo. Just open **Wino.sln** solution in your IDE and launch.
|
||||
|
||||
## Project Architecture
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,77 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="ColorHashSharp" Version="1.1.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.1-build.4" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250926-build.2293" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" Version="0.1.250926-build.2293" />
|
||||
<PackageVersion Include="EmailValidation" Version="1.3.0" />
|
||||
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Graph" Version="5.99.0" />
|
||||
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageVersion Include="Microsoft.Identity.Client" Version="4.79.2" />
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.79.2" />
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.2" />
|
||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
|
||||
<PackageVersion Include="MimeKit" Version="4.14.0" />
|
||||
<PackageVersion Include="morelinq" Version="4.4.0" />
|
||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||
<PackageVersion Include="NodaTime" Version="3.2.3" />
|
||||
<PackageVersion Include="Sentry.Serilog" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.0" />
|
||||
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="3.119.1" />
|
||||
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.1" />
|
||||
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="10.0.1" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.1" />
|
||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
|
||||
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.4.1" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Google.Apis.Auth" Version="1.73.0" />
|
||||
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.73.0.3993" />
|
||||
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.73.0.3987" />
|
||||
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.72.0.3973" />
|
||||
<PackageVersion Include="HtmlKit" Version="1.2.0" />
|
||||
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||
<PackageVersion Include="TimePeriodLibrary.NET" Version="2.1.6" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.1" />
|
||||
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
|
||||
<PackageVersion Include="WinUIEx" Version="2.9.0" />
|
||||
<!-- Testing packages -->
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||
<PackageVersion Include="xunit" Version="2.9.0" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -25,6 +25,7 @@ I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I f
|
||||
- API integration for Outlook and Gmail
|
||||
- IMAP/SMTP support for custom mail servers
|
||||
- Send, receive, mark as (read,important,spam etc), move mails.
|
||||
- Linked/Merged Accounts
|
||||
- Toast notifications with background sync.
|
||||
- Instant startup performance
|
||||
- Offline use / search.
|
||||
@@ -43,7 +44,7 @@ Download latest version of Wino Mail from Microsoft Store for free.
|
||||
|
||||
## Beta Releases
|
||||
|
||||
Stable releases will always be distributed on Microsoft Store. However, beta releases will be distributed in [GitHub Releases](https://github.com/bkaankose/Wino-Mail/releases). Please keep in mind that beta releases might not be for daily use, only for testing purposes and recommended for experienced users or developers.
|
||||
Stable releases will always be distributed on Microsoft Store. However, beta releases will be distributed in [GitHub Releases](https://github.com/bkaankose/Wino-Mail/releases). Please keep in mind that beta releases might not be for daily use, only for testing purposes and recommended for experienced users or developers. Beta releases are also managed manually. Therefore, code in the repository might be ahead of the released Beta version at the moment. Make sure to compare versions before tryout out the Beta version.
|
||||
|
||||
These releases are distributed as side-loaded packages. To install them, download the **.msixbundle** file in GitHub releases and [follow the steps explained here.](https://learn.microsoft.com/en-us/windows/application-management/sideload-apps-in-windows)
|
||||
|
||||
@@ -52,10 +53,6 @@ These releases are distributed as side-loaded packages. To install them, downloa
|
||||
|
||||
Check out the [contribution guidelines](/CONTRIBUTING.md) before diving into the source code or opening an issue. There are multiple ways to contribute and all of them are explained in detail there.
|
||||
|
||||
#### Attention
|
||||
|
||||
Sources here **does not belong to the Store version of Wino Mail. It belongs to beta release as of April 17 2024.** I've been working on a big patch for couple months already and the code here includes those changes, but these changes are not yet released to Microsoft Store. Therefore, if you'd like to contribute, please validate the bug before in beta version and start working on it. I will delete this text from here once this big patch goes alive in the Store, so everything will be aligned then.
|
||||
|
||||
## Donate
|
||||
|
||||
Your donations will motivate me more to work on Wino in my spare time and cover the expenses to keep [project's website](https://www.winomail.app/) alive.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"AttributesTolerance": 2,
|
||||
"KeepFirstAttributeOnSameLine": false,
|
||||
"MaxAttributeCharactersPerLine": 0,
|
||||
"MaxAttributesPerLine": 1,
|
||||
"NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransform, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter",
|
||||
"SeparateByGroups": false,
|
||||
"AttributeIndentation": 0,
|
||||
"AttributeIndentationStyle": 1,
|
||||
"RemoveDesignTimeReferences": false,
|
||||
"IgnoreDesignTimeReferencePrefix": false,
|
||||
"EnableAttributeReordering": true,
|
||||
"AttributeOrderingRuleGroups": [
|
||||
"x:Class",
|
||||
"xmlns, xmlns:x",
|
||||
"xmlns:*",
|
||||
"x:Key, Key, x:Name, Name, x:Uid, Uid, Title",
|
||||
"Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom",
|
||||
"Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight",
|
||||
"Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex",
|
||||
"*:*, *",
|
||||
"PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint",
|
||||
"mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText",
|
||||
"Storyboard.*, From, To, Duration"
|
||||
],
|
||||
"FirstLineAttributes": "",
|
||||
"OrderAttributesByName": true,
|
||||
"PutEndingBracketOnNewLine": false,
|
||||
"RemoveEndingTagOfEmptyElement": true,
|
||||
"SpaceBeforeClosingSlash": true,
|
||||
"RootElementLineBreakRule": 0,
|
||||
"ReorderVSM": 2,
|
||||
"ReorderGridChildren": false,
|
||||
"ReorderCanvasChildren": false,
|
||||
"ReorderSetters": 0,
|
||||
"FormatMarkupExtension": true,
|
||||
"NoNewLineMarkupExtensions": "x:Bind, Binding",
|
||||
"ThicknessSeparator": 2,
|
||||
"ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin",
|
||||
"FormatOnSave": true,
|
||||
"CommentPadding": 2,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Authentication;
|
||||
|
||||
public abstract class BaseAuthenticator
|
||||
{
|
||||
public abstract MailProviderType ProviderType { get; }
|
||||
protected IAuthenticatorConfig AuthenticatorConfig { get; }
|
||||
|
||||
protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
|
||||
{
|
||||
|
||||
AuthenticatorConfig = authenticatorConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using Google.Apis.Util.Store;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Authentication;
|
||||
|
||||
namespace Wino.Authentication;
|
||||
|
||||
public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
|
||||
{
|
||||
public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig)
|
||||
{
|
||||
}
|
||||
|
||||
public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId;
|
||||
public bool ProposeCopyAuthURL { get; set; }
|
||||
|
||||
public override MailProviderType ProviderType => MailProviderType.Gmail;
|
||||
|
||||
/// <summary>
|
||||
/// Generates the token information for the given account.
|
||||
/// For gmail, interactivity is automatically handled when you get the token.
|
||||
/// </summary>
|
||||
/// <param name="account">Account to get token for.</param>
|
||||
public Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
||||
=> GetTokenInformationAsync(account);
|
||||
|
||||
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
|
||||
{
|
||||
var userCredential = await GetGoogleUserCredentialAsync(account);
|
||||
|
||||
if (userCredential.Token.IsStale)
|
||||
{
|
||||
await userCredential.RefreshTokenAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
return new TokenInformationEx(userCredential.Token.AccessToken, account.Address);
|
||||
}
|
||||
|
||||
private Task<UserCredential> GetGoogleUserCredentialAsync(MailAccount account)
|
||||
{
|
||||
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
|
||||
{
|
||||
ClientId = ClientId
|
||||
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Identity.Client;
|
||||
using Microsoft.Identity.Client.Broker;
|
||||
using Microsoft.Identity.Client.Extensions.Msal;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Authentication;
|
||||
|
||||
namespace Wino.Authentication;
|
||||
|
||||
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
||||
{
|
||||
private const string TokenCacheFileName = "OutlookCache.bin";
|
||||
private bool isTokenCacheAttached = false;
|
||||
|
||||
// Outlook
|
||||
private const string Authority = "https://login.microsoftonline.com/common";
|
||||
|
||||
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
||||
|
||||
private readonly IPublicClientApplication _publicClientApplication;
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||
|
||||
public OutlookAuthenticator(INativeAppService nativeAppService,
|
||||
IApplicationConfiguration applicationConfiguration,
|
||||
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
_applicationConfiguration = applicationConfiguration;
|
||||
|
||||
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
|
||||
|
||||
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
|
||||
{
|
||||
Title = "Wino Mail",
|
||||
ListOperatingSystemAccounts = true,
|
||||
};
|
||||
|
||||
PublicClientApplicationBuilder outlookAppBuilder = null;
|
||||
|
||||
// Being created from an app notification.
|
||||
// This is where we avoid all interactive shit for authentication.
|
||||
if (nativeAppService.GetCoreWindowHwnd == null)
|
||||
{
|
||||
outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
|
||||
.WithDefaultRedirectUri()
|
||||
.WithBroker(options)
|
||||
.WithAuthority(Authority);
|
||||
}
|
||||
else
|
||||
{
|
||||
outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
|
||||
.WithBroker(options)
|
||||
.WithParentActivityOrWindow(_nativeAppService.GetCoreWindowHwnd)
|
||||
.WithDefaultRedirectUri()
|
||||
.WithAuthority(Authority);
|
||||
}
|
||||
|
||||
_publicClientApplication = outlookAppBuilder.Build();
|
||||
}
|
||||
|
||||
public string[] Scope => AuthenticatorConfig.OutlookScope;
|
||||
|
||||
private async Task EnsureTokenCacheAttachedAsync()
|
||||
{
|
||||
if (!isTokenCacheAttached)
|
||||
{
|
||||
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
|
||||
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
|
||||
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
|
||||
|
||||
isTokenCacheAttached = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
|
||||
{
|
||||
await EnsureTokenCacheAttachedAsync();
|
||||
|
||||
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(
|
||||
a => string.Equals(a.Username?.Trim(), account.Address?.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (storedAccount == null)
|
||||
return await GenerateTokenInformationAsync(account);
|
||||
|
||||
try
|
||||
{
|
||||
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
|
||||
|
||||
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
||||
}
|
||||
catch (MsalUiRequiredException)
|
||||
{
|
||||
// Somehow MSAL is not able to refresh the token silently.
|
||||
// Force interactive login which will include calendar scopes.
|
||||
// The calling code should update account.IsCalendarAccessGranted = true after successful authentication.
|
||||
|
||||
return await GenerateTokenInformationAsync(account);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
||||
{
|
||||
try
|
||||
{
|
||||
await EnsureTokenCacheAttachedAsync();
|
||||
|
||||
// Interactive authentication required but window doesn't exist.
|
||||
// This can happen when being called from a notification background task and the token is expired.
|
||||
// Force account attention;
|
||||
|
||||
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
|
||||
|
||||
AuthenticationResult authResult = await _publicClientApplication
|
||||
.AcquireTokenInteractive(Scope)
|
||||
.ExecuteAsync();
|
||||
|
||||
// If the account is null, it means it's the initial creation of it.
|
||||
// If not, make sure the authenticated user address matches the username.
|
||||
// When people refresh their token, accounts must match.
|
||||
|
||||
if (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new AuthenticationException("Authenticated address does not match with your account address. If you are signing with a Office365, it is not officially supported yet.");
|
||||
}
|
||||
|
||||
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
||||
}
|
||||
catch (MsalClientException msalClientException)
|
||||
{
|
||||
if (msalClientException.ErrorCode == "authentication_canceled" || msalClientException.ErrorCode == "access_denied")
|
||||
throw new AccountSetupCanceledException();
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new AuthenticationException(Translator.Exception_UnknowErrorDuringAuthentication, new Exception(Translator.Exception_TokenGenerationFailed));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<RootNamespace>Wino.Authentication</RootNamespace>
|
||||
<Platforms>x86;x64;arm64</Platforms>
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Diagnostics" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Google.Apis.Auth" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Broker" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" />
|
||||
<PackageReference Include="Sentry.Serilog" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,34 +0,0 @@
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.Background;
|
||||
using Wino.Core.Domain;
|
||||
|
||||
namespace Wino.BackgroundTasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a toast notification to notify user when the Store update happens.
|
||||
/// </summary>
|
||||
public sealed class AppUpdatedTask : IBackgroundTask
|
||||
{
|
||||
public void Run(IBackgroundTaskInstance taskInstance)
|
||||
{
|
||||
var def = taskInstance.GetDeferral();
|
||||
|
||||
var builder = new ToastContentBuilder();
|
||||
builder.SetToastScenario(ToastScenario.Default);
|
||||
|
||||
Package package = Package.Current;
|
||||
PackageId packageId = package.Id;
|
||||
PackageVersion version = packageId.Version;
|
||||
|
||||
var versionText = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
|
||||
|
||||
builder.AddText(Translator.Notifications_WinoUpdatedTitle);
|
||||
builder.AddText(string.Format(Translator.Notifications_WinoUpdatedMessage, versionText));
|
||||
|
||||
builder.Show();
|
||||
|
||||
def.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Wino.BackgroundTasks")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Wino.BackgroundTasks")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2023")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: ComVisible(false)]
|
||||
@@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Windows.ApplicationModel.Background;
|
||||
using Windows.Storage;
|
||||
using Wino.Core;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Core.UWP;
|
||||
using Wino.Services;
|
||||
|
||||
namespace Wino.BackgroundTasks
|
||||
{
|
||||
public sealed class SessionConnectedTask : IBackgroundTask
|
||||
{
|
||||
public async void Run(IBackgroundTaskInstance taskInstance)
|
||||
{
|
||||
var def = taskInstance.GetDeferral();
|
||||
|
||||
try
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.RegisterCoreServices();
|
||||
services.RegisterCoreUWPServices();
|
||||
|
||||
var providere = services.BuildServiceProvider();
|
||||
|
||||
var backgroundTaskService = providere.GetService<IBackgroundSynchronizer>();
|
||||
var dbService = providere.GetService<IDatabaseService>();
|
||||
var logInitializer = providere.GetService<ILogInitializer>();
|
||||
|
||||
logInitializer.SetupLogger(ApplicationData.Current.LocalFolder.Path);
|
||||
|
||||
await dbService.InitializeAsync();
|
||||
await backgroundTaskService.RunBackgroundSynchronizationAsync(Core.Domain.Enums.BackgroundSynchronizationReason.SessionConnected);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Background synchronization failed from background task.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
def.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}</ProjectGuid>
|
||||
<OutputType>winmdobj</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Wino.BackgroundTasks</RootNamespace>
|
||||
<AssemblyName>Wino.BackgroundTasks</AssemblyName>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AllowCrossPlatformRetargeting>false</AllowCrossPlatformRetargeting>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\ARM\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<OutputPath>bin\ARM\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
|
||||
<PlatformTarget>ARM64</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\ARM64\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
|
||||
<PlatformTarget>ARM64</PlatformTarget>
|
||||
<OutputPath>bin\ARM64\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x64\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<OutputPath>bin\x64\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AppUpdatedTask.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SessionConnectedTask.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
|
||||
<Version>6.2.14</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications">
|
||||
<Version>7.1.3</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj">
|
||||
<Project>{CF3312E5-5DA0-4867-9945-49EA7598AF1F}</Project>
|
||||
<Name>Wino.Core.Domain</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Wino.Core.UWP\Wino.Core.UWP.csproj">
|
||||
<Project>{395f19ba-1e42-495c-9db5-1a6f537fccb8}</Project>
|
||||
<Name>Wino.Core.UWP</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj">
|
||||
<Project>{e6b1632a-8901-41e8-9ddf-6793c7698b0b}</Project>
|
||||
<Name>Wino.Core</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<SDKReference Include="WindowsDesktop, Version=10.0.22621.0">
|
||||
<Name>Windows Desktop Extensions for the UWP</Name>
|
||||
</SDKReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||
<VisualStudioVersion>14.0</VisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
|
||||
namespace Wino.Calendar.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for managing calendar account settings.
|
||||
/// </summary>
|
||||
public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewModel
|
||||
{
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial MailAccount Account { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendar AccountCalendar { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountColorHex { get; set; } = "#0078D4";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSyncEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsPrimaryCalendar { get; set; }
|
||||
|
||||
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = new ObservableCollection<ShowAsOption>();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ShowAsOption SelectedDefaultShowAsOption { get; set; }
|
||||
|
||||
public CalendarAccountSettingsPageViewModel(ICalendarService calendarService, IAccountService accountService)
|
||||
{
|
||||
_calendarService = calendarService;
|
||||
_accountService = accountService;
|
||||
|
||||
// Initialize ShowAs options
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters is AccountCalendar selectedCalendar)
|
||||
{
|
||||
Account = await _accountService.GetAccountAsync(selectedCalendar.AccountId);
|
||||
AccountCalendar = await _calendarService.GetAccountCalendarAsync(selectedCalendar.Id) ?? selectedCalendar;
|
||||
}
|
||||
else if (parameters is Guid accountId)
|
||||
{
|
||||
Account = await _accountService.GetAccountAsync(accountId);
|
||||
var calendars = await _calendarService.GetAccountCalendarsAsync(accountId);
|
||||
AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Account == null || AccountCalendar == null)
|
||||
return;
|
||||
|
||||
// Initialize properties from AccountCalendar
|
||||
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
|
||||
IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
|
||||
IsPrimaryCalendar = AccountCalendar.IsPrimary;
|
||||
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
|
||||
}
|
||||
|
||||
partial void OnAccountColorHexChanged(string value)
|
||||
{
|
||||
if (AccountCalendar != null && !string.IsNullOrEmpty(value))
|
||||
{
|
||||
AccountCalendar.BackgroundColorHex = value;
|
||||
SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnIsSyncEnabledChanged(bool value)
|
||||
{
|
||||
if (AccountCalendar != null)
|
||||
{
|
||||
AccountCalendar.IsSynchronizationEnabled = value;
|
||||
SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnIsPrimaryCalendarChanged(bool value)
|
||||
{
|
||||
if (AccountCalendar != null)
|
||||
{
|
||||
AccountCalendar.IsPrimary = value;
|
||||
SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
|
||||
{
|
||||
if (AccountCalendar != null && value != null)
|
||||
{
|
||||
AccountCalendar.DefaultShowAs = value.ShowAs;
|
||||
SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void SaveChangesAsync()
|
||||
{
|
||||
if (AccountCalendar == null)
|
||||
return;
|
||||
|
||||
await _calendarService.UpdateAccountCalendarAsync(AccountCalendar);
|
||||
|
||||
// Send message to update UI
|
||||
Messenger.Send(new CalendarListUpdated(AccountCalendar));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Serilog;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Calendar.ViewModels.Interfaces;
|
||||
using Wino.Core.Domain.Collections;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.Server;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Calendar.ViewModels;
|
||||
|
||||
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
IRecipient<VisibleDateRangeChangedMessage>,
|
||||
IRecipient<CalendarEnableStatusChangedMessage>,
|
||||
IRecipient<NavigateManageAccountsRequested>,
|
||||
IRecipient<CalendarDisplayTypeChangedMessage>,
|
||||
IRecipient<AccountRemovedMessage>
|
||||
{
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
public IStatePersistanceService StatePersistenceService { get; }
|
||||
public IAccountCalendarStateService AccountCalendarStateService { get; }
|
||||
public INavigationService NavigationService { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private int _selectedMenuItemIndex = -1;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCalendarEnabled;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display date of the calendar.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private DateTimeOffset _displayDate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private DateRange highlightedDateRange;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private int _selectedDateNavigationHeaderIndex;
|
||||
|
||||
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
|
||||
|
||||
// For updating account calendars asynchronously.
|
||||
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
||||
|
||||
public CalendarAppShellViewModel(IPreferencesService preferencesService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IAccountService accountService,
|
||||
ICalendarService calendarService,
|
||||
IAccountCalendarStateService accountCalendarStateService,
|
||||
INavigationService navigationService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_calendarService = calendarService;
|
||||
|
||||
AccountCalendarStateService = accountCalendarStateService;
|
||||
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
||||
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
|
||||
|
||||
NavigationService = navigationService;
|
||||
PreferencesService = preferencesService;
|
||||
|
||||
StatePersistenceService = statePersistanceService;
|
||||
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
{
|
||||
base.OnDispatcherAssigned();
|
||||
|
||||
AccountCalendarStateService.Dispatcher = Dispatcher;
|
||||
}
|
||||
|
||||
private void PrefefencesChanged(object sender, string e)
|
||||
{
|
||||
if (e == nameof(StatePersistenceService.CalendarDisplayType))
|
||||
{
|
||||
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
|
||||
|
||||
UpdateDateNavigationHeaderItems();
|
||||
|
||||
// Change the calendar.
|
||||
DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate()));
|
||||
}
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
// Account list may have changed while this shell was inactive.
|
||||
if (mode == NavigationMode.Back)
|
||||
{
|
||||
await InitializeAccountCalendarsAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateDateNavigationHeaderItems();
|
||||
|
||||
await InitializeAccountCalendarsAsync();
|
||||
|
||||
TodayClicked();
|
||||
}
|
||||
|
||||
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
||||
{
|
||||
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
||||
// Reporting all these changes one by one to the UI is not efficient and may cause problems in the future.
|
||||
|
||||
// Update all calendar states at once.
|
||||
try
|
||||
{
|
||||
await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
|
||||
|
||||
foreach (var calendar in e.AccountCalendars)
|
||||
{
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error while waiting for account calendar update semaphore.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_accountCalendarUpdateSemaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
|
||||
=> await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false);
|
||||
|
||||
private async Task InitializeAccountCalendarsAsync()
|
||||
{
|
||||
await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendars());
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
||||
var calendarViewModels = new List<AccountCalendarViewModel>();
|
||||
|
||||
foreach (var calendar in accountCalendars)
|
||||
{
|
||||
var calendarViewModel = new AccountCalendarViewModel(account, calendar);
|
||||
|
||||
calendarViewModels.Add(calendarViewModel);
|
||||
}
|
||||
|
||||
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
|
||||
|
||||
await Dispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceNavigateCalendarDate()
|
||||
{
|
||||
if (SelectedMenuItemIndex == -1)
|
||||
{
|
||||
var args = new CalendarPageNavigationArgs()
|
||||
{
|
||||
NavigationDate = _navigationDate ?? DateTime.Now.Date
|
||||
};
|
||||
|
||||
// Already on calendar. Just navigate.
|
||||
NavigationService.Navigate(WinoPage.CalendarPage, args);
|
||||
|
||||
_navigationDate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedMenuItemIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue)
|
||||
{
|
||||
switch (newValue)
|
||||
{
|
||||
case -1:
|
||||
ForceNavigateCalendarDate();
|
||||
break;
|
||||
case 0:
|
||||
NavigationService.Navigate(WinoPage.ManageAccountsPage);
|
||||
break;
|
||||
case 1:
|
||||
NavigationService.Navigate(WinoPage.SettingsPage);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Sync()
|
||||
{
|
||||
// Sync all calendars.
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Type = CalendarSynchronizationType.CalendarEvents
|
||||
});
|
||||
|
||||
Messenger.Send(t);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When calendar type switches, we need to navigate to the most ideal date.
|
||||
/// This method returns that date.
|
||||
/// </summary>
|
||||
private DateTime GetDisplayTypeSwitchDate()
|
||||
{
|
||||
var settings = PreferencesService.GetCurrentCalendarSettings();
|
||||
switch (StatePersistenceService.CalendarDisplayType)
|
||||
{
|
||||
case CalendarDisplayType.Day:
|
||||
if (HighlightedDateRange.IsInRange(DateTime.Now)) return DateTime.Now.Date;
|
||||
|
||||
return HighlightedDateRange.StartDate;
|
||||
case CalendarDisplayType.Week:
|
||||
if (HighlightedDateRange == null || HighlightedDateRange.IsInRange(DateTime.Now))
|
||||
{
|
||||
return DateTime.Now.Date.GetWeekStartDateForDate(settings.FirstDayOfWeek);
|
||||
}
|
||||
|
||||
return HighlightedDateRange.StartDate.GetWeekStartDateForDate(settings.FirstDayOfWeek);
|
||||
case CalendarDisplayType.WorkWeek:
|
||||
break;
|
||||
case CalendarDisplayType.Month:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return DateTime.Today.Date;
|
||||
}
|
||||
|
||||
private DateTime? _navigationDate;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
|
||||
#region Commands
|
||||
|
||||
[RelayCommand]
|
||||
private void TodayClicked()
|
||||
{
|
||||
_navigationDate = DateTime.Now.Date;
|
||||
|
||||
ForceNavigateCalendarDate();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
|
||||
{
|
||||
_navigationDate = clickedDateArgs.ClickedDate;
|
||||
|
||||
ForceNavigateCalendarDate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
UnregisterRecipients();
|
||||
|
||||
Messenger.Register<VisibleDateRangeChangedMessage>(this);
|
||||
Messenger.Register<CalendarEnableStatusChangedMessage>(this);
|
||||
Messenger.Register<NavigateManageAccountsRequested>(this);
|
||||
Messenger.Register<CalendarDisplayTypeChangedMessage>(this);
|
||||
Messenger.Register<AccountRemovedMessage>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<VisibleDateRangeChangedMessage>(this);
|
||||
Messenger.Unregister<CalendarEnableStatusChangedMessage>(this);
|
||||
Messenger.Unregister<NavigateManageAccountsRequested>(this);
|
||||
Messenger.Unregister<CalendarDisplayTypeChangedMessage>(this);
|
||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the header navigation items based on visible date range and calendar type.
|
||||
/// </summary>
|
||||
private void UpdateDateNavigationHeaderItems()
|
||||
{
|
||||
var settings = PreferencesService.GetCurrentCalendarSettings();
|
||||
var cultureInfo = settings.CultureInfo ?? CultureInfo.CurrentUICulture;
|
||||
|
||||
var visibleRange = HighlightedDateRange ?? new DateRange(DateTime.Today, DateTime.Today.AddDays(1));
|
||||
var headerText = GetHeaderText(visibleRange, cultureInfo);
|
||||
|
||||
DateNavigationHeaderItems.ReplaceRange([headerText]);
|
||||
SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
private string GetHeaderText(DateRange visibleRange, CultureInfo cultureInfo)
|
||||
{
|
||||
var startDate = visibleRange.StartDate.Date;
|
||||
var endDate = visibleRange.EndDate.Date > startDate ? visibleRange.EndDate.Date.AddDays(-1) : startDate;
|
||||
|
||||
switch (StatePersistenceService.CalendarDisplayType)
|
||||
{
|
||||
case CalendarDisplayType.Day:
|
||||
return startDate.ToString("MMMM d, dddd", cultureInfo);
|
||||
case CalendarDisplayType.Week:
|
||||
case CalendarDisplayType.WorkWeek:
|
||||
if (startDate.Month == endDate.Month && startDate.Year == endDate.Year)
|
||||
{
|
||||
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("%d", cultureInfo)}";
|
||||
}
|
||||
|
||||
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("MMMM d", cultureInfo)}";
|
||||
case CalendarDisplayType.Month:
|
||||
return GetDominantMonthHeaderText(startDate, endDate, cultureInfo);
|
||||
default:
|
||||
return startDate.ToString("d", cultureInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDominantMonthHeaderText(DateTime startDate, DateTime endDate, CultureInfo cultureInfo)
|
||||
{
|
||||
if (endDate < startDate)
|
||||
{
|
||||
endDate = startDate;
|
||||
}
|
||||
|
||||
var monthDayCounts = new Dictionary<(int Year, int Month), int>();
|
||||
|
||||
for (var day = startDate; day <= endDate; day = day.AddDays(1))
|
||||
{
|
||||
var key = (day.Year, day.Month);
|
||||
|
||||
if (monthDayCounts.TryGetValue(key, out var count))
|
||||
{
|
||||
monthDayCounts[key] = count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
monthDayCounts[key] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
var dominantKey = (Year: startDate.Year, Month: startDate.Month);
|
||||
var dominantCount = -1;
|
||||
|
||||
foreach (var pair in monthDayCounts)
|
||||
{
|
||||
if (pair.Value > dominantCount)
|
||||
{
|
||||
dominantCount = pair.Value;
|
||||
dominantKey = pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
return new DateTime(dominantKey.Year, dominantKey.Month, 1).ToString("Y", cultureInfo);
|
||||
}
|
||||
|
||||
partial void OnHighlightedDateRangeChanged(DateRange value) => UpdateDateNavigationHeaderItems();
|
||||
|
||||
public async void Receive(CalendarEnableStatusChangedMessage message)
|
||||
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
|
||||
|
||||
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
|
||||
|
||||
public void Receive(CalendarDisplayTypeChangedMessage message)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsVerticalCalendar));
|
||||
UpdateDateNavigationHeaderItems();
|
||||
}
|
||||
|
||||
public async void Receive(AccountRemovedMessage message)
|
||||
=> await InitializeAccountCalendarsAsync();
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Translations;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
|
||||
namespace Wino.Calendar.ViewModels;
|
||||
|
||||
public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial double CellHourHeight { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedFirstDayOfWeekIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool Is24HourHeaders { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial TimeSpan WorkingHourStart { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial TimeSpan WorkingHourEnd { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<string> DayNames { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int WorkingDayStartIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int WorkingDayEndIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<string> ReminderOptions { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedDefaultReminderIndex { get; set; }
|
||||
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public ObservableCollection<MailAccount> Accounts { get; } = new ObservableCollection<MailAccount>();
|
||||
|
||||
private readonly bool _isLoaded = false;
|
||||
|
||||
public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService, IAccountService accountService)
|
||||
{
|
||||
PreferencesService = preferencesService;
|
||||
_calendarService = calendarService;
|
||||
_accountService = accountService;
|
||||
|
||||
var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
|
||||
|
||||
var cultureInfo = new CultureInfo(currentLanguageLanguageCode);
|
||||
|
||||
// Populate the day names list
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
DayNames.Add(cultureInfo.DateTimeFormat.DayNames[i]);
|
||||
}
|
||||
|
||||
var cultureFirstDayName = cultureInfo.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek);
|
||||
SelectedFirstDayOfWeekIndex = DayNames.IndexOf(cultureFirstDayName);
|
||||
Is24HourHeaders = preferencesService.Prefer24HourTimeFormat;
|
||||
WorkingHourStart = preferencesService.WorkingHourStart;
|
||||
WorkingHourEnd = preferencesService.WorkingHourEnd;
|
||||
CellHourHeight = preferencesService.HourHeight;
|
||||
WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
|
||||
WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
|
||||
|
||||
// Initialize reminder options
|
||||
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
|
||||
ReminderOptions.Add("None");
|
||||
foreach (var minutes in predefinedMinutes)
|
||||
{
|
||||
var displayText = minutes switch
|
||||
{
|
||||
>= 60 => $"{minutes / 60} Hour{(minutes / 60 > 1 ? "s" : "")}",
|
||||
_ => $"{minutes} Minute{(minutes > 1 ? "s" : "")}"
|
||||
};
|
||||
ReminderOptions.Add(displayText);
|
||||
}
|
||||
|
||||
// Set selected index based on current default reminder setting
|
||||
if (preferencesService.DefaultReminderDurationInSeconds == 0)
|
||||
{
|
||||
SelectedDefaultReminderIndex = 0; // None
|
||||
}
|
||||
else
|
||||
{
|
||||
var minutes = (int)(preferencesService.DefaultReminderDurationInSeconds / 60);
|
||||
var index = Array.IndexOf(predefinedMinutes, minutes);
|
||||
SelectedDefaultReminderIndex = index >= 0 ? index + 1 : 0;
|
||||
}
|
||||
|
||||
_isLoaded = true;
|
||||
|
||||
// Load accounts with calendar support
|
||||
LoadAccountsAsync();
|
||||
}
|
||||
|
||||
private async void LoadAccountsAsync()
|
||||
{
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
|
||||
await Dispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
Accounts.Clear();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
Accounts.Add(account);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void NavigateToAccountSettings(MailAccount account)
|
||||
{
|
||||
if (account == null) return;
|
||||
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
string.Format(Translator.CalendarAccountSettings_Description, account.Name),
|
||||
WinoPage.CalendarAccountSettingsPage,
|
||||
account.Id));
|
||||
}
|
||||
|
||||
partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
|
||||
partial void OnIs24HourHeadersChanged(bool value) => SaveSettings();
|
||||
partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
|
||||
partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
|
||||
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
|
||||
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
|
||||
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
|
||||
partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings();
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
|
||||
PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch
|
||||
{
|
||||
0 => DayOfWeek.Sunday,
|
||||
1 => DayOfWeek.Monday,
|
||||
2 => DayOfWeek.Tuesday,
|
||||
3 => DayOfWeek.Wednesday,
|
||||
4 => DayOfWeek.Thursday,
|
||||
5 => DayOfWeek.Friday,
|
||||
6 => DayOfWeek.Saturday,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
PreferencesService.WorkingDayStart = WorkingDayStartIndex switch
|
||||
{
|
||||
0 => DayOfWeek.Sunday,
|
||||
1 => DayOfWeek.Monday,
|
||||
2 => DayOfWeek.Tuesday,
|
||||
3 => DayOfWeek.Wednesday,
|
||||
4 => DayOfWeek.Thursday,
|
||||
5 => DayOfWeek.Friday,
|
||||
6 => DayOfWeek.Saturday,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch
|
||||
{
|
||||
0 => DayOfWeek.Sunday,
|
||||
1 => DayOfWeek.Monday,
|
||||
2 => DayOfWeek.Tuesday,
|
||||
3 => DayOfWeek.Wednesday,
|
||||
4 => DayOfWeek.Thursday,
|
||||
5 => DayOfWeek.Friday,
|
||||
6 => DayOfWeek.Saturday,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders;
|
||||
PreferencesService.WorkingHourStart = WorkingHourStart;
|
||||
PreferencesService.WorkingHourEnd = WorkingHourEnd;
|
||||
PreferencesService.HourHeight = CellHourHeight;
|
||||
|
||||
// Save default reminder setting
|
||||
if (SelectedDefaultReminderIndex == 0)
|
||||
{
|
||||
PreferencesService.DefaultReminderDurationInSeconds = 0; // None
|
||||
}
|
||||
else
|
||||
{
|
||||
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
|
||||
var minutes = predefinedMinutes[SelectedDefaultReminderIndex - 1];
|
||||
PreferencesService.DefaultReminderDurationInSeconds = minutes * 60;
|
||||
}
|
||||
|
||||
Messenger.Send(new CalendarSettingsUpdatedMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Data;
|
||||
|
||||
public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar
|
||||
{
|
||||
public MailAccount Account { get; }
|
||||
public AccountCalendar AccountCalendar { get; }
|
||||
|
||||
public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar)
|
||||
{
|
||||
Account = account;
|
||||
AccountCalendar = accountCalendar;
|
||||
|
||||
IsChecked = accountCalendar.IsExtended;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isChecked;
|
||||
|
||||
partial void OnIsCheckedChanged(bool value) => IsExtended = value;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => AccountCalendar.Name;
|
||||
set => SetProperty(AccountCalendar.Name, value, AccountCalendar, (u, n) => u.Name = n);
|
||||
}
|
||||
|
||||
public string TextColorHex
|
||||
{
|
||||
get => AccountCalendar.TextColorHex;
|
||||
set => SetProperty(AccountCalendar.TextColorHex, value, AccountCalendar, (u, t) => u.TextColorHex = t);
|
||||
}
|
||||
|
||||
public string BackgroundColorHex
|
||||
{
|
||||
get => AccountCalendar.BackgroundColorHex;
|
||||
set => SetProperty(AccountCalendar.BackgroundColorHex, value, AccountCalendar, (u, b) => u.BackgroundColorHex = b);
|
||||
}
|
||||
|
||||
public bool IsExtended
|
||||
{
|
||||
get => AccountCalendar.IsExtended;
|
||||
set => SetProperty(AccountCalendar.IsExtended, value, AccountCalendar, (u, i) => u.IsExtended = i);
|
||||
}
|
||||
|
||||
public bool IsPrimary
|
||||
{
|
||||
get => AccountCalendar.IsPrimary;
|
||||
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
|
||||
}
|
||||
|
||||
public bool IsSynchronizationEnabled
|
||||
{
|
||||
get => AccountCalendar.IsSynchronizationEnabled;
|
||||
set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i);
|
||||
}
|
||||
|
||||
public Guid AccountId
|
||||
{
|
||||
get => AccountCalendar.AccountId;
|
||||
set => SetProperty(AccountCalendar.AccountId, value, AccountCalendar, (u, a) => u.AccountId = a);
|
||||
}
|
||||
|
||||
public string RemoteCalendarId
|
||||
{
|
||||
get => AccountCalendar.RemoteCalendarId;
|
||||
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
|
||||
}
|
||||
|
||||
public CalendarItemShowAs DefaultShowAs
|
||||
{
|
||||
get => AccountCalendar.DefaultShowAs;
|
||||
set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s);
|
||||
}
|
||||
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
|
||||
public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Extensions;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Data;
|
||||
|
||||
public partial class CalendarAttachmentViewModel : ObservableObject
|
||||
{
|
||||
public CalendarAttachment Attachment { get; }
|
||||
|
||||
public Guid Id => Attachment.Id;
|
||||
public string FileName => Attachment.FileName;
|
||||
public string ReadableSize { get; }
|
||||
public MailAttachmentType AttachmentType { get; }
|
||||
public bool IsDownloaded => Attachment.IsDownloaded;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
public CalendarAttachmentViewModel(CalendarAttachment attachment)
|
||||
{
|
||||
Attachment = attachment;
|
||||
ReadableSize = attachment.Size.GetBytesReadable();
|
||||
|
||||
var extension = Path.GetExtension(FileName);
|
||||
AttachmentType = GetAttachmentType(extension);
|
||||
}
|
||||
|
||||
private MailAttachmentType GetAttachmentType(string extension)
|
||||
{
|
||||
if (string.IsNullOrEmpty(extension))
|
||||
return MailAttachmentType.None;
|
||||
|
||||
switch (extension.ToLower())
|
||||
{
|
||||
case ".exe":
|
||||
return MailAttachmentType.Executable;
|
||||
case ".rar":
|
||||
return MailAttachmentType.RarArchive;
|
||||
case ".zip":
|
||||
return MailAttachmentType.Archive;
|
||||
case ".ogg":
|
||||
case ".mp3":
|
||||
case ".wav":
|
||||
case ".aac":
|
||||
case ".alac":
|
||||
return MailAttachmentType.Audio;
|
||||
case ".mp4":
|
||||
case ".wmv":
|
||||
case ".avi":
|
||||
case ".flv":
|
||||
return MailAttachmentType.Video;
|
||||
case ".pdf":
|
||||
return MailAttachmentType.PDF;
|
||||
case ".htm":
|
||||
case ".html":
|
||||
return MailAttachmentType.HTML;
|
||||
case ".png":
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
case ".gif":
|
||||
case ".jiff":
|
||||
return MailAttachmentType.Image;
|
||||
default:
|
||||
return MailAttachmentType.Other;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Itenso.TimePeriod;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Data;
|
||||
|
||||
public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, ICalendarItemViewModel
|
||||
{
|
||||
public CalendarItem CalendarItem { get; }
|
||||
|
||||
public string Title => CalendarItem.Title;
|
||||
|
||||
public Guid Id => CalendarItem.Id;
|
||||
|
||||
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start date converted to user's local timezone for display.
|
||||
/// The underlying CalendarItem stores dates according to their timezone.
|
||||
/// </summary>
|
||||
public DateTime StartDate
|
||||
{
|
||||
get
|
||||
{
|
||||
// Get start date in user's local timezone
|
||||
return CalendarItem.LocalStartDate;
|
||||
}
|
||||
set
|
||||
{
|
||||
// When setting from UI (in local time), convert to event's timezone for storage.
|
||||
CalendarItem.StartDate = value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end date converted to user's local timezone for display.
|
||||
/// The underlying CalendarItem stores dates according to their timezone.
|
||||
/// </summary>
|
||||
public DateTime EndDate
|
||||
{
|
||||
get
|
||||
{
|
||||
// Get end date in user's local timezone
|
||||
return CalendarItem.LocalEndDate;
|
||||
}
|
||||
}
|
||||
|
||||
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time period in local time.
|
||||
/// </summary>
|
||||
public ITimePeriod Period
|
||||
{
|
||||
get
|
||||
{
|
||||
// Return a period using local times for UI display
|
||||
return new TimeRange(StartDate, EndDate);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
|
||||
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
|
||||
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
|
||||
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
|
||||
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The period of the day where this item is currently being displayed.
|
||||
/// Used for multi-day event title formatting.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(DisplayTitle))]
|
||||
public partial ITimePeriod DisplayingPeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calendar settings for time formatting.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(DisplayTitle))]
|
||||
public partial CalendarSettings CalendarSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display title based on the current displaying period.
|
||||
/// </summary>
|
||||
public string DisplayTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
if (DisplayingPeriod == null || CalendarSettings == null)
|
||||
return Title;
|
||||
|
||||
return GetDisplayTitle(DisplayingPeriod, CalendarSettings);
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
|
||||
|
||||
public CalendarItemViewModel(CalendarItem calendarItem)
|
||||
{
|
||||
CalendarItem = calendarItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the underlying CalendarItem with new data and raises property change notifications.
|
||||
/// </summary>
|
||||
/// <param name="calendarItem">The updated calendar item data.</param>
|
||||
public void UpdateFrom(CalendarItem calendarItem)
|
||||
{
|
||||
if (calendarItem == null || calendarItem.Id != CalendarItem.Id)
|
||||
return;
|
||||
|
||||
// Update all mutable properties
|
||||
CalendarItem.Title = calendarItem.Title;
|
||||
CalendarItem.Description = calendarItem.Description;
|
||||
CalendarItem.Location = calendarItem.Location;
|
||||
CalendarItem.StartDate = calendarItem.StartDate;
|
||||
CalendarItem.StartTimeZone = calendarItem.StartTimeZone;
|
||||
CalendarItem.EndTimeZone = calendarItem.EndTimeZone;
|
||||
CalendarItem.DurationInSeconds = calendarItem.DurationInSeconds;
|
||||
CalendarItem.Recurrence = calendarItem.Recurrence;
|
||||
CalendarItem.RecurringCalendarItemId = calendarItem.RecurringCalendarItemId;
|
||||
CalendarItem.OrganizerDisplayName = calendarItem.OrganizerDisplayName;
|
||||
CalendarItem.OrganizerEmail = calendarItem.OrganizerEmail;
|
||||
CalendarItem.IsLocked = calendarItem.IsLocked;
|
||||
CalendarItem.IsHidden = calendarItem.IsHidden;
|
||||
CalendarItem.CustomEventColorHex = calendarItem.CustomEventColorHex;
|
||||
CalendarItem.HtmlLink = calendarItem.HtmlLink;
|
||||
CalendarItem.Status = calendarItem.Status;
|
||||
CalendarItem.Visibility = calendarItem.Visibility;
|
||||
CalendarItem.ShowAs = calendarItem.ShowAs;
|
||||
CalendarItem.UpdatedAt = calendarItem.UpdatedAt;
|
||||
CalendarItem.AssignedCalendar = calendarItem.AssignedCalendar;
|
||||
|
||||
// Raise property changed for all bindable properties
|
||||
OnPropertyChanged(nameof(Title));
|
||||
OnPropertyChanged(nameof(StartDate));
|
||||
OnPropertyChanged(nameof(EndDate));
|
||||
OnPropertyChanged(nameof(DurationInSeconds));
|
||||
OnPropertyChanged(nameof(Period));
|
||||
OnPropertyChanged(nameof(IsAllDayEvent));
|
||||
OnPropertyChanged(nameof(IsMultiDayEvent));
|
||||
OnPropertyChanged(nameof(IsRecurringEvent));
|
||||
OnPropertyChanged(nameof(IsRecurringChild));
|
||||
OnPropertyChanged(nameof(IsRecurringParent));
|
||||
OnPropertyChanged(nameof(AssignedCalendar));
|
||||
OnPropertyChanged(nameof(DisplayTitle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display title for this calendar item when rendered in a specific day.
|
||||
/// </summary>
|
||||
public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings)
|
||||
{
|
||||
if (!IsMultiDayEvent)
|
||||
return Title;
|
||||
|
||||
var periodRelation = Period.GetRelation(displayingPeriod);
|
||||
|
||||
if (periodRelation == PeriodRelation.StartInside || periodRelation == PeriodRelation.EnclosingStartTouching)
|
||||
{
|
||||
// Event starts within this day: "HH:mm -> Title"
|
||||
return $"{calendarSettings.GetTimeString(StartDate.TimeOfDay)} -> {Title}";
|
||||
}
|
||||
else if (periodRelation == PeriodRelation.EndInside || periodRelation == PeriodRelation.EnclosingEndTouching)
|
||||
{
|
||||
// Event ends within this day: "Title <- HH:mm"
|
||||
return $"{Title} <- {calendarSettings.GetTimeString(EndDate.TimeOfDay)}";
|
||||
}
|
||||
else if (periodRelation == PeriodRelation.Enclosing)
|
||||
{
|
||||
// Event spans the entire day
|
||||
return $"{Translator.CalendarItemAllDay} {Title}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return Title;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => CalendarItem.Title;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Data;
|
||||
|
||||
public partial class GroupedAccountCalendarViewModel : ObservableObject
|
||||
{
|
||||
public event EventHandler CollectiveSelectionStateChanged;
|
||||
public event EventHandler<AccountCalendarViewModel> CalendarSelectionStateChanged;
|
||||
|
||||
public MailAccount Account { get; }
|
||||
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
|
||||
|
||||
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
|
||||
{
|
||||
Account = account;
|
||||
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
|
||||
|
||||
ManageIsCheckedState();
|
||||
|
||||
foreach (var calendarViewModel in calendarViewModels)
|
||||
{
|
||||
calendarViewModel.PropertyChanged += CalendarPropertyChanged;
|
||||
}
|
||||
|
||||
AccountCalendars.CollectionChanged += CalendarListUpdated;
|
||||
}
|
||||
|
||||
private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
foreach (AccountCalendarViewModel calendar in e.NewItems)
|
||||
{
|
||||
calendar.PropertyChanged += CalendarPropertyChanged;
|
||||
}
|
||||
}
|
||||
else if (e.Action == NotifyCollectionChangedAction.Remove)
|
||||
{
|
||||
foreach (AccountCalendarViewModel calendar in e.OldItems)
|
||||
{
|
||||
calendar.PropertyChanged -= CalendarPropertyChanged;
|
||||
}
|
||||
}
|
||||
else if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
foreach (AccountCalendarViewModel calendar in e.OldItems)
|
||||
{
|
||||
calendar.PropertyChanged -= CalendarPropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is AccountCalendarViewModel viewModel)
|
||||
{
|
||||
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
|
||||
{
|
||||
ManageIsCheckedState();
|
||||
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsExpanded { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool? IsCheckedState { get; set; } = true;
|
||||
|
||||
private bool _isExternalPropChangeBlocked = false;
|
||||
|
||||
private void ManageIsCheckedState()
|
||||
{
|
||||
if (_isExternalPropChangeBlocked) return;
|
||||
|
||||
_isExternalPropChangeBlocked = true;
|
||||
|
||||
if (AccountCalendars.All(c => c.IsChecked))
|
||||
{
|
||||
IsCheckedState = true;
|
||||
}
|
||||
else if (AccountCalendars.All(c => !c.IsChecked))
|
||||
{
|
||||
IsCheckedState = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCheckedState = null;
|
||||
}
|
||||
|
||||
_isExternalPropChangeBlocked = false;
|
||||
}
|
||||
|
||||
partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
|
||||
{
|
||||
if (_isExternalPropChangeBlocked) return;
|
||||
|
||||
// Update is triggered by user on the three-state checkbox.
|
||||
// We should not report all changes one by one.
|
||||
|
||||
_isExternalPropChangeBlocked = true;
|
||||
|
||||
if (newValue == null)
|
||||
{
|
||||
// Only primary calendars must be checked.
|
||||
|
||||
foreach (var calendar in AccountCalendars)
|
||||
{
|
||||
UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var calendar in AccountCalendars)
|
||||
{
|
||||
UpdateCalendarCheckedState(calendar, newValue.GetValueOrDefault());
|
||||
}
|
||||
}
|
||||
|
||||
_isExternalPropChangeBlocked = false;
|
||||
|
||||
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void UpdateCalendarCheckedState(AccountCalendarViewModel accountCalendarViewModel, bool newValue, bool ignoreValueCheck = false)
|
||||
{
|
||||
var currentValue = accountCalendarViewModel.IsChecked;
|
||||
|
||||
if (currentValue == newValue && !ignoreValueCheck) return;
|
||||
|
||||
accountCalendarViewModel.IsChecked = newValue;
|
||||
|
||||
// No need to report.
|
||||
if (_isExternalPropChangeBlocked == true) return;
|
||||
|
||||
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Serilog;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
|
||||
namespace Wino.Calendar.ViewModels;
|
||||
|
||||
public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
||||
{
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||
private readonly INavigationService _navigationService;
|
||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||
|
||||
public CalendarSettings CurrentSettings { get; }
|
||||
public INativeAppService NativeAppService => _nativeAppService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsDarkWebviewRenderer { get; set; }
|
||||
|
||||
public ObservableCollection<CalendarAttachmentViewModel> Attachments { get; } = new ObservableCollection<CalendarAttachmentViewModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current event has attachments.
|
||||
/// </summary>
|
||||
public bool HasAttachments => Attachments.Count > 0;
|
||||
|
||||
#region Details
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
|
||||
[NotifyPropertyChangedFor(nameof(CanEditSeries))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCurrentUserOrganizer))]
|
||||
[NotifyPropertyChangedFor(nameof(CurrentRsvpText))]
|
||||
[NotifyPropertyChangedFor(nameof(CurrentRsvpStatus))]
|
||||
public partial CalendarItemViewModel CurrentEvent { get; set; }
|
||||
|
||||
partial void OnCurrentEventChanged(CalendarItemViewModel value)
|
||||
{
|
||||
// Notify the view to re-render the description
|
||||
Messenger.Send(new CalendarDescriptionRenderingRequested());
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CalendarItemViewModel SeriesParent { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial List<Reminder> Reminders { get; set; }
|
||||
|
||||
public ObservableCollection<ReminderOption> ReminderOptions { get; } = new ObservableCollection<ReminderOption>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the event is part of a recurring series (as a child occurrence).
|
||||
/// Used to enable "View Series" functionality.
|
||||
/// </summary>
|
||||
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the "Edit Series" button should be visible.
|
||||
/// Only visible for child occurrences of recurring events, not for master events or single events.
|
||||
/// </summary>
|
||||
public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current user is the organizer of the event.
|
||||
/// Used to determine if the user can invite attendees or modify the event.
|
||||
/// </summary>
|
||||
public bool IsCurrentUserOrganizer => CurrentEvent?.Attendees?.Any(a => a.IsOrganizer) ?? true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Show As Options
|
||||
|
||||
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = new ObservableCollection<ShowAsOption>();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ShowAsOption SelectedShowAsOption { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region RSVP Panel
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsRsvpPanelVisible { get; set; }
|
||||
|
||||
public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage);
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IncludeRsvpMessage))]
|
||||
public partial string RsvpMessage { get; set; } = string.Empty;
|
||||
|
||||
public ObservableCollection<RsvpStatusOption> RsvpStatusOptions { get; } = new ObservableCollection<RsvpStatusOption>();
|
||||
|
||||
public CalendarItemStatus CurrentRsvpStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
return CurrentEvent?.CalendarItem?.Status ?? CalendarItemStatus.NotResponded;
|
||||
}
|
||||
}
|
||||
|
||||
public string CurrentRsvpText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurrentEvent?.CalendarItem == null) return Translator.CalendarEventResponse_Accept;
|
||||
|
||||
return CurrentEvent.CalendarItem.Status switch
|
||||
{
|
||||
CalendarItemStatus.Accepted => Translator.CalendarEventResponse_AcceptedResponse,
|
||||
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse,
|
||||
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse,
|
||||
CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded,
|
||||
_ => Translator.CalendarEventResponse_NotResponded
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public EventDetailsPageViewModel(ICalendarService calendarService,
|
||||
INativeAppService nativeAppService,
|
||||
IPreferencesService preferencesService,
|
||||
IMailDialogService dialogService,
|
||||
IWinoRequestDelegator winoRequestDelegator,
|
||||
INavigationService navigationService,
|
||||
IUnderlyingThemeService underlyingThemeService)
|
||||
{
|
||||
_calendarService = calendarService;
|
||||
_nativeAppService = nativeAppService;
|
||||
_preferencesService = preferencesService;
|
||||
_dialogService = dialogService;
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_navigationService = navigationService;
|
||||
_underlyingThemeService = underlyingThemeService;
|
||||
|
||||
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
|
||||
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
|
||||
|
||||
// Initialize Show As options
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
|
||||
SelectedShowAsOption = ShowAsOptions[2]; // Default to Busy
|
||||
|
||||
// Initialize RSVP status options
|
||||
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Accepted));
|
||||
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Tentative));
|
||||
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Cancelled));
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters == null || parameters is not CalendarItemTarget args)
|
||||
return;
|
||||
|
||||
await LoadCalendarItemTargetAsync(args);
|
||||
}
|
||||
|
||||
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
|
||||
{
|
||||
base.OnCalendarItemUpdated(calendarItem, source);
|
||||
|
||||
// If the current event was updated, reload it
|
||||
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
|
||||
{
|
||||
// Reflect client-side optimistic changes immediately; fallback to DB for server updates.
|
||||
if (source == CalendarItemUpdateSource.ClientUpdated || source == CalendarItemUpdateSource.ClientReverted)
|
||||
{
|
||||
var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
|
||||
CurrentEvent = new CalendarItemViewModel(calendarItem)
|
||||
{
|
||||
IsBusy = source == CalendarItemUpdateSource.ClientUpdated
|
||||
};
|
||||
|
||||
foreach (var attendee in previousAttendees)
|
||||
{
|
||||
CurrentEvent.Attendees.Add(attendee);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh from DB when update comes from server sync.
|
||||
var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id);
|
||||
if (refreshedEvent != null)
|
||||
{
|
||||
CurrentEvent = new CalendarItemViewModel(refreshedEvent);
|
||||
await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
|
||||
{
|
||||
base.OnCalendarItemDeleted(calendarItem);
|
||||
|
||||
// If the current event was deleted, navigate back
|
||||
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
|
||||
{
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentEventItem = await _calendarService.GetCalendarItemTargetAsync(target);
|
||||
|
||||
if (currentEventItem == null)
|
||||
return;
|
||||
|
||||
CurrentEvent = new CalendarItemViewModel(currentEventItem);
|
||||
|
||||
await LoadAttendeesAsync(currentEventItem.Id, currentEventItem);
|
||||
|
||||
// Initialize SelectedShowAsOption based on current event's ShowAs
|
||||
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2];
|
||||
|
||||
// Load reminders for this calendar item
|
||||
Reminders = await _calendarService.GetRemindersAsync(currentEventItem.Id);
|
||||
InitializeReminderOptions();
|
||||
|
||||
// Load attachments
|
||||
await LoadAttachmentsAsync(currentEventItem.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem)
|
||||
{
|
||||
CurrentEvent.Attendees.Clear();
|
||||
|
||||
var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
|
||||
|
||||
// Separate organizer from other attendees to ensure organizer is always first
|
||||
var organizer = attendees.FirstOrDefault(a => a.IsOrganizer);
|
||||
var nonOrganizerAttendees = attendees.Where(a => !a.IsOrganizer).ToList();
|
||||
|
||||
// If the organizer is in the list, add them first
|
||||
if (organizer != null)
|
||||
{
|
||||
CurrentEvent.Attendees.Add(organizer);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
|
||||
{
|
||||
// If the organizer is not in the attendees list, create and add them first
|
||||
var organizerAttendee = new CalendarEventAttendee
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = calendarItem.Id,
|
||||
Name = calendarItem.OrganizerDisplayName ?? calendarItem.OrganizerEmail,
|
||||
Email = calendarItem.OrganizerEmail,
|
||||
IsOrganizer = true,
|
||||
AttendenceStatus = AttendeeStatus.Accepted
|
||||
};
|
||||
CurrentEvent.Attendees.Add(organizerAttendee);
|
||||
}
|
||||
|
||||
// Add all other attendees after the organizer
|
||||
foreach (var item in nonOrganizerAttendees)
|
||||
{
|
||||
CurrentEvent.Attendees.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAttachmentsAsync(Guid calendarItemId)
|
||||
{
|
||||
Attachments.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var attachments = await _calendarService.GetAttachmentsAsync(calendarItemId);
|
||||
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
Attachments.Add(new CalendarAttachmentViewModel(attachment));
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasAttachments));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error loading attachments: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeReminderOptions()
|
||||
{
|
||||
ReminderOptions.Clear();
|
||||
|
||||
// Add predefined options from service
|
||||
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
|
||||
var predefinedOptions = predefinedMinutes.Select(m => new ReminderOption(m)).ToList();
|
||||
|
||||
// Add custom reminders from synced data
|
||||
if (Reminders != null)
|
||||
{
|
||||
foreach (var reminder in Reminders)
|
||||
{
|
||||
// Convert seconds to minutes
|
||||
var minutesDiff = (int)(reminder.DurationInSeconds / 60);
|
||||
|
||||
// Check if this is a custom value not in predefined list
|
||||
if (!predefinedMinutes.Contains(minutesDiff))
|
||||
{
|
||||
predefinedOptions.Add(new ReminderOption(minutesDiff, isCustom: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by minutes descending and add to collection
|
||||
foreach (var option in predefinedOptions.OrderByDescending(o => o.Minutes))
|
||||
{
|
||||
ReminderOptions.Add(option);
|
||||
}
|
||||
|
||||
// Set selected state based on current reminders
|
||||
if (Reminders != null)
|
||||
{
|
||||
foreach (var reminder in Reminders)
|
||||
{
|
||||
// Convert seconds to minutes
|
||||
var minutesDiff = (int)(reminder.DurationInSeconds / 60);
|
||||
|
||||
var matchingOption = ReminderOptions.FirstOrDefault(o => o.Minutes == minutesDiff);
|
||||
matchingOption?.IsSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (CurrentEvent == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Capture original state BEFORE making any changes for potential revert
|
||||
var originalItem = await _calendarService.GetCalendarItemAsync(CurrentEvent.CalendarItem.Id);
|
||||
var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.Id);
|
||||
|
||||
// Get selected reminder options
|
||||
var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList();
|
||||
|
||||
// Create separate Reminder entities for each selected option
|
||||
var newReminders = new List<Reminder>();
|
||||
|
||||
foreach (var option in selectedOptions)
|
||||
{
|
||||
var durationInSeconds = option.Minutes * 60; // Convert minutes to seconds
|
||||
|
||||
newReminders.Add(new Reminder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = CurrentEvent.Id,
|
||||
DurationInSeconds = durationInSeconds,
|
||||
ReminderType = CalendarItemReminderType.Popup
|
||||
});
|
||||
}
|
||||
|
||||
// Save reminders to database
|
||||
await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.Id, newReminders);
|
||||
Reminders = newReminders;
|
||||
|
||||
// Update ShowAs if changed
|
||||
if (SelectedShowAsOption != null)
|
||||
{
|
||||
CurrentEvent.CalendarItem.ShowAs = SelectedShowAsOption.ShowAs;
|
||||
}
|
||||
|
||||
// Update the calendar item and attendees in database
|
||||
await _calendarService.UpdateCalendarItemAsync(CurrentEvent.CalendarItem, CurrentEvent.Attendees.ToList());
|
||||
|
||||
// Queue the update request to synchronizer with original state for revert capability
|
||||
var preparationRequest = new CalendarOperationPreparationRequest(
|
||||
CalendarSynchronizerOperation.UpdateEvent,
|
||||
CurrentEvent.CalendarItem,
|
||||
CurrentEvent.Attendees.ToList(),
|
||||
ResponseMessage: null,
|
||||
OriginalItem: originalItem,
|
||||
OriginalAttendees: originalAttendees);
|
||||
|
||||
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
||||
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error saving event: {ex.Message}");
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AttachmentSaveFailedTitle,
|
||||
ex.Message,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
if (CurrentEvent == null) return;
|
||||
|
||||
// If the event is a master recurring event, ask for confirmation
|
||||
if (CurrentEvent.IsRecurringParent)
|
||||
{
|
||||
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
||||
Translator.DialogMessage_DeleteRecurringSeriesMessage,
|
||||
Translator.DialogMessage_DeleteRecurringSeriesTitle,
|
||||
Translator.Buttons_Delete);
|
||||
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var preparationRequest = new CalendarOperationPreparationRequest(
|
||||
CalendarSynchronizerOperation.DeleteEvent,
|
||||
CurrentEvent.CalendarItem,
|
||||
null);
|
||||
|
||||
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
||||
|
||||
// Navigate back after successful deletion
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error deleting calendar event: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task JoinOnlineAsync()
|
||||
{
|
||||
if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink))
|
||||
return Task.CompletedTask;
|
||||
|
||||
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleRsvpPanel()
|
||||
{
|
||||
IsRsvpPanelVisible = !IsRsvpPanelVisible;
|
||||
|
||||
if (IsRsvpPanelVisible && CurrentEvent?.CalendarItem != null)
|
||||
{
|
||||
// Initialize selection based on current status
|
||||
foreach (var item in RsvpStatusOptions)
|
||||
{
|
||||
item.IsSelected = CurrentEvent?.CalendarItem?.Status == item.Status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseRsvpPanel()
|
||||
{
|
||||
IsRsvpPanelVisible = false;
|
||||
RsvpMessage = string.Empty;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SendRsvpResponse(AttendeeStatus status)
|
||||
{
|
||||
if (CurrentEvent == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Get the optional response message if user wants to include it
|
||||
var responseMessage = IncludeRsvpMessage ? RsvpMessage : null;
|
||||
|
||||
// Map status to operation
|
||||
CalendarSynchronizerOperation operation = status switch
|
||||
{
|
||||
AttendeeStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent,
|
||||
AttendeeStatus.Tentative => CalendarSynchronizerOperation.TentativeEvent,
|
||||
AttendeeStatus.Declined => CalendarSynchronizerOperation.DeclineEvent,
|
||||
_ => throw new InvalidOperationException($"Invalid RSVP status: {status}")
|
||||
};
|
||||
|
||||
// Create preparation request with the optional message
|
||||
var preparationRequest = new CalendarOperationPreparationRequest(
|
||||
operation,
|
||||
CurrentEvent.CalendarItem,
|
||||
null,
|
||||
responseMessage);
|
||||
|
||||
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
||||
|
||||
// Reload attendees to get the updated status from the server
|
||||
await LoadAttendeesAsync(CurrentEvent.CalendarItem.Id, CurrentEvent.CalendarItem);
|
||||
|
||||
OnPropertyChanged(nameof(CurrentRsvpText));
|
||||
OnPropertyChanged(nameof(CurrentRsvpStatus));
|
||||
|
||||
CloseRsvpPanel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error sending RSVP response: {ex.Message}");
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AttachmentSaveFailedTitle,
|
||||
ex.Message,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ViewSeriesAsync()
|
||||
{
|
||||
if (CurrentEvent == null || !CurrentEvent.IsRecurringChild) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Get the master event from the recurring series
|
||||
var masterEventId = CurrentEvent.CalendarItem.RecurringCalendarItemId.Value;
|
||||
var masterEvent = await _calendarService.GetCalendarItemAsync(masterEventId);
|
||||
|
||||
if (masterEvent == null) return;
|
||||
|
||||
// Load the master event without navigation
|
||||
var target = new CalendarItemTarget(masterEvent, CalendarEventTargetType.Series);
|
||||
await LoadCalendarItemTargetAsync(target);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error loading series: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
|
||||
{
|
||||
if (attachmentViewModel == null || CurrentEvent?.CalendarItem == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
attachmentViewModel.IsBusy = true;
|
||||
|
||||
// If not downloaded, download it first
|
||||
if (!attachmentViewModel.IsDownloaded)
|
||||
{
|
||||
await DownloadAttachmentAsync(attachmentViewModel);
|
||||
}
|
||||
|
||||
// Launch the file
|
||||
if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) &&
|
||||
File.Exists(attachmentViewModel.Attachment.LocalFilePath))
|
||||
{
|
||||
await _nativeAppService.LaunchFileAsync(attachmentViewModel.Attachment.LocalFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to open calendar attachment.");
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AttachmentOpenFailedTitle,
|
||||
Translator.Info_AttachmentOpenFailedMessage,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
attachmentViewModel.IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
|
||||
{
|
||||
if (attachmentViewModel == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
attachmentViewModel.IsBusy = true;
|
||||
|
||||
var pickedPath = await _dialogService.PickWindowsFolderAsync();
|
||||
if (string.IsNullOrEmpty(pickedPath)) return;
|
||||
|
||||
// Download if not already downloaded
|
||||
if (!attachmentViewModel.IsDownloaded)
|
||||
{
|
||||
await DownloadAttachmentAsync(attachmentViewModel);
|
||||
}
|
||||
|
||||
// Copy to selected location
|
||||
if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) &&
|
||||
File.Exists(attachmentViewModel.Attachment.LocalFilePath))
|
||||
{
|
||||
var destinationPath = Path.Combine(pickedPath, attachmentViewModel.FileName);
|
||||
File.Copy(attachmentViewModel.Attachment.LocalFilePath, destinationPath, overwrite: true);
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AttachmentSaveSuccessTitle,
|
||||
Translator.Info_AttachmentSaveSuccessMessage,
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to save calendar attachment.");
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AttachmentSaveFailedTitle,
|
||||
Translator.Info_AttachmentSaveFailedMessage,
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
attachmentViewModel.IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
|
||||
{
|
||||
if (CurrentEvent?.CalendarItem == null) return;
|
||||
|
||||
// Create attachments folder for this calendar item
|
||||
var attachmentsFolder = Path.Combine(
|
||||
_nativeAppService.GetCalendarAttachmentsFolderPath(),
|
||||
CurrentEvent.CalendarItem.Id.ToString());
|
||||
|
||||
Directory.CreateDirectory(attachmentsFolder);
|
||||
|
||||
var localFilePath = Path.Combine(attachmentsFolder, attachmentViewModel.FileName);
|
||||
|
||||
// Download attachment using synchronizer
|
||||
await SynchronizationManager.Instance.DownloadCalendarAttachmentAsync(
|
||||
CurrentEvent.CalendarItem,
|
||||
attachmentViewModel.Attachment,
|
||||
localFilePath);
|
||||
|
||||
// Mark as downloaded
|
||||
await _calendarService.MarkAttachmentDownloadedAsync(
|
||||
attachmentViewModel.Id,
|
||||
localFilePath);
|
||||
|
||||
// Update view model
|
||||
attachmentViewModel.Attachment.IsDownloaded = true;
|
||||
attachmentViewModel.Attachment.LocalFilePath = localFilePath;
|
||||
OnPropertyChanged(nameof(attachmentViewModel.IsDownloaded));
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ReminderOption : ObservableObject
|
||||
{
|
||||
public int Minutes { get; }
|
||||
public bool IsCustom { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Minutes >= 60)
|
||||
{
|
||||
var hours = Minutes / 60;
|
||||
return hours == 1 ? "1 Hour" : $"{hours} Hours";
|
||||
}
|
||||
return Minutes == 1 ? "1 Minute" : $"{Minutes} Minutes";
|
||||
}
|
||||
}
|
||||
|
||||
public ReminderOption(int minutes, bool isCustom = false)
|
||||
{
|
||||
Minutes = minutes;
|
||||
IsCustom = isCustom;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ShowAsOption : ObservableObject
|
||||
{
|
||||
public CalendarItemShowAs ShowAs { get; }
|
||||
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
{
|
||||
return ShowAs switch
|
||||
{
|
||||
CalendarItemShowAs.Free => Translator.CalendarShowAs_Free,
|
||||
CalendarItemShowAs.Tentative => Translator.CalendarShowAs_Tentative,
|
||||
CalendarItemShowAs.Busy => Translator.CalendarShowAs_Busy,
|
||||
CalendarItemShowAs.OutOfOffice => Translator.CalendarShowAs_OutOfOffice,
|
||||
CalendarItemShowAs.WorkingElsewhere => Translator.CalendarShowAs_WorkingElsewhere,
|
||||
_ => Translator.CalendarShowAs_Busy
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ShowAsOption(CalendarItemShowAs showAs)
|
||||
{
|
||||
ShowAs = showAs;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RsvpStatusOption : ObservableObject
|
||||
{
|
||||
public CalendarItemStatus Status { get; }
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get
|
||||
{
|
||||
return Status switch
|
||||
{
|
||||
CalendarItemStatus.Accepted => Translator.CalendarEventResponse_Accept,
|
||||
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_Tentative,
|
||||
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_Decline,
|
||||
_ => Translator.CalendarEventResponse_Accept
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
public RsvpStatusOption(CalendarItemStatus status)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Collections;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Interfaces;
|
||||
|
||||
public interface IAccountCalendarStateService : INotifyPropertyChanged
|
||||
{
|
||||
IDispatcher Dispatcher { get; set; }
|
||||
ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; }
|
||||
|
||||
event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
|
||||
event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
|
||||
|
||||
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
|
||||
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
|
||||
public void ClearGroupedAccountCalendars();
|
||||
|
||||
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
|
||||
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
|
||||
|
||||
/// <summary>
|
||||
/// Enumeration of currently selected calendars.
|
||||
/// </summary>
|
||||
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
|
||||
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
|
||||
ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Messages;
|
||||
|
||||
public class CalendarItemDoubleTappedMessage
|
||||
{
|
||||
public CalendarItemDoubleTappedMessage(CalendarItemViewModel calendarItemViewModel)
|
||||
{
|
||||
CalendarItemViewModel = calendarItemViewModel;
|
||||
}
|
||||
|
||||
public CalendarItemViewModel CalendarItemViewModel { get; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Messages;
|
||||
|
||||
public class CalendarItemRightTappedMessage
|
||||
{
|
||||
public CalendarItemRightTappedMessage(CalendarItemViewModel calendarItemViewModel)
|
||||
{
|
||||
CalendarItemViewModel = calendarItemViewModel;
|
||||
}
|
||||
|
||||
public CalendarItemViewModel CalendarItemViewModel { get; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Messages;
|
||||
|
||||
public class CalendarItemTappedMessage
|
||||
{
|
||||
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
|
||||
{
|
||||
CalendarItemViewModel = calendarItemViewModel;
|
||||
ClickedPeriod = clickedPeriod;
|
||||
}
|
||||
|
||||
public CalendarItemViewModel CalendarItemViewModel { get; }
|
||||
public CalendarDayModel ClickedPeriod { get; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Platforms>x86;x64;arm64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TimePeriodLibrary.NET" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Wino.Core.ViewModels\Wino.Core.ViewModels.csproj" />
|
||||
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
|
||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,7 +0,0 @@
|
||||
<Application
|
||||
x:Class="Wino.Calendar.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Wino.Calendar">
|
||||
|
||||
</Application>
|
||||
@@ -1,100 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.Activation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Controls.Primitives;
|
||||
using Windows.UI.Xaml.Data;
|
||||
using Windows.UI.Xaml.Input;
|
||||
using Windows.UI.Xaml.Media;
|
||||
using Windows.UI.Xaml.Navigation;
|
||||
|
||||
namespace Wino.Calendar
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
sealed partial class App : Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.Suspending += OnSuspending;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched normally by the end user. Other entry points
|
||||
/// will be used such as when the application is launched to open a specific file.
|
||||
/// </summary>
|
||||
/// <param name="e">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs e)
|
||||
{
|
||||
Frame rootFrame = Window.Current.Content as Frame;
|
||||
|
||||
// Do not repeat app initialization when the Window already has content,
|
||||
// just ensure that the window is active
|
||||
if (rootFrame == null)
|
||||
{
|
||||
// Create a Frame to act as the navigation context and navigate to the first page
|
||||
rootFrame = new Frame();
|
||||
|
||||
rootFrame.NavigationFailed += OnNavigationFailed;
|
||||
|
||||
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
|
||||
{
|
||||
//TODO: Load state from previously suspended application
|
||||
}
|
||||
|
||||
// Place the frame in the current Window
|
||||
Window.Current.Content = rootFrame;
|
||||
}
|
||||
|
||||
if (e.PrelaunchActivated == false)
|
||||
{
|
||||
if (rootFrame.Content == null)
|
||||
{
|
||||
// When the navigation stack isn't restored navigate to the first page,
|
||||
// configuring the new page by passing required information as a navigation
|
||||
// parameter
|
||||
rootFrame.Navigate(typeof(MainPage), e.Arguments);
|
||||
}
|
||||
// Ensure the current window is active
|
||||
Window.Current.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when Navigation to a certain page fails
|
||||
/// </summary>
|
||||
/// <param name="sender">The Frame which failed navigation</param>
|
||||
/// <param name="e">Details about the navigation failure</param>
|
||||
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
|
||||
{
|
||||
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when application execution is being suspended. Application state is saved
|
||||
/// without knowing whether the application will be terminated or resumed with the contents
|
||||
/// of memory still intact.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the suspend request.</param>
|
||||
/// <param name="e">Details about the suspend request.</param>
|
||||
private void OnSuspending(object sender, SuspendingEventArgs e)
|
||||
{
|
||||
var deferral = e.SuspendingOperation.GetDeferral();
|
||||
//TODO: Save application state and stop any background activity
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,14 +0,0 @@
|
||||
<Page
|
||||
x:Class="Wino.Calendar.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Wino.Calendar"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Controls.Primitives;
|
||||
using Windows.UI.Xaml.Data;
|
||||
using Windows.UI.Xaml.Input;
|
||||
using Windows.UI.Xaml.Media;
|
||||
using Windows.UI.Xaml.Navigation;
|
||||
|
||||
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
|
||||
|
||||
namespace Wino.Calendar
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class MainPage : Page
|
||||
{
|
||||
public MainPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
IgnorableNamespaces="uap mp">
|
||||
|
||||
<!-- Publisher Cache Folders -->
|
||||
<Extensions>
|
||||
<Extension Category="windows.publisherCacheFolders">
|
||||
<PublisherCacheFolders>
|
||||
<Folder Name="WinoShared" />
|
||||
</PublisherCacheFolders>
|
||||
</Extension>
|
||||
</Extensions>
|
||||
|
||||
<Identity
|
||||
Name="58272BurakKSE.WinoCalendar"
|
||||
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="f047b7dd-96ec-4d54-a862-9321e271e449" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Wino Calendar</DisplayName>
|
||||
<PublisherDisplayName>Burak KÖSE</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="Wino.Calendar.App">
|
||||
<uap:VisualElements
|
||||
DisplayName="Wino Calendar"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png"
|
||||
Description="Wino.Calendar"
|
||||
BackgroundColor="transparent">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Wino.Calendar")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Wino.Calendar")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2023")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: ComVisible(false)]
|
||||
@@ -1,31 +0,0 @@
|
||||
<!--
|
||||
This file contains Runtime Directives used by .NET Native. The defaults here are suitable for most
|
||||
developers. However, you can modify these parameters to modify the behavior of the .NET Native
|
||||
optimizer.
|
||||
|
||||
Runtime Directives are documented at https://go.microsoft.com/fwlink/?LinkID=391919
|
||||
|
||||
To fully enable reflection for App1.MyClass and all of its public/private members
|
||||
<Type Name="App1.MyClass" Dynamic="Required All"/>
|
||||
|
||||
To enable dynamic creation of the specific instantiation of AppClass<T> over System.Int32
|
||||
<TypeInstantiation Name="App1.AppClass" Arguments="System.Int32" Activate="Required Public" />
|
||||
|
||||
Using the Namespace directive to apply reflection policy to all the types in a particular namespace
|
||||
<Namespace Name="DataClasses.ViewModels" Serialize="All" />
|
||||
-->
|
||||
|
||||
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
|
||||
<Application>
|
||||
<!--
|
||||
An Assembly element with Name="*Application*" applies to all assemblies in
|
||||
the application package. The asterisks are not wildcards.
|
||||
-->
|
||||
<Assembly Name="*Application*" Dynamic="Required All" />
|
||||
|
||||
|
||||
<!-- Add your application specific runtime directives here. -->
|
||||
|
||||
|
||||
</Application>
|
||||
</Directives>
|
||||
@@ -1,172 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
|
||||
<ProjectGuid>{600F4979-DB7E-409D-B7DA-B60BE4C55C35}</ProjectGuid>
|
||||
<OutputType>AppContainerExe</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Wino.Calendar</RootNamespace>
|
||||
<AssemblyName>Wino.Calendar</AssemblyName>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<WindowsXamlEnableOverview>true</WindowsXamlEnableOverview>
|
||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||
<PackageCertificateThumbprint>125A5273FCFE8D551C3FED87F67C87A663E98F1B</PackageCertificateThumbprint>
|
||||
<PackageCertificateKeyFile />
|
||||
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\ARM\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
|
||||
<OutputPath>bin\ARM\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\ARM64\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>ARM64</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
|
||||
<OutputPath>bin\ARM64\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>ARM64</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x64\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<OutputPath>bin\x64\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="App.xaml.cs">
|
||||
<DependentUpon>App.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="MainPage.xaml.cs">
|
||||
<DependentUpon>MainPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AppxManifest Include="Package.appxmanifest">
|
||||
<SubType>Designer</SubType>
|
||||
</AppxManifest>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Package.StoreAssociation.xml" />
|
||||
<Content Include="Properties\Default.rd.xml" />
|
||||
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="Assets\StoreLogo.png" />
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="App.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Page Include="MainPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
|
||||
<Version>6.2.14</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||
<VisualStudioVersion>14.0</VisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Wino.Core.Domain;
|
||||
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
public partial class BasicTypesJsonContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Wino.Core.Domain;
|
||||
|
||||
public static class CalendarReminderSnoozeOptions
|
||||
{
|
||||
private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30];
|
||||
|
||||
public static IReadOnlyList<int> GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds)
|
||||
{
|
||||
var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60);
|
||||
|
||||
if (reminderMinutes <= 0)
|
||||
return [];
|
||||
|
||||
var maxSnoozeMinutes = reminderMinutes;
|
||||
var defaultReminderMinutes = (int)Math.Max(0, defaultReminderDurationInSeconds / 60);
|
||||
|
||||
if (defaultReminderMinutes > 0)
|
||||
maxSnoozeMinutes = Math.Min(maxSnoozeMinutes, defaultReminderMinutes);
|
||||
|
||||
return SupportedSnoozeMinutes.Where(minutes => minutes <= maxSnoozeMinutes).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Itenso.TimePeriod;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Core.Domain.Collections;
|
||||
|
||||
public class CalendarEventCollection
|
||||
{
|
||||
public event EventHandler<ICalendarItem> CalendarItemAdded;
|
||||
public event EventHandler<ICalendarItem> CalendarItemRemoved;
|
||||
public event EventHandler<ICalendarItem> CalendarItemUpdated;
|
||||
|
||||
public event EventHandler CalendarItemsCleared;
|
||||
|
||||
private ObservableRangeCollection<ICalendarItem> _internalRegularEvents = [];
|
||||
private ObservableRangeCollection<ICalendarItem> _internalAllDayEvents = [];
|
||||
|
||||
public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; }
|
||||
public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; } // TODO: Rename this to include multi-day events.
|
||||
public ITimePeriod Period { get; }
|
||||
public CalendarSettings Settings { get; }
|
||||
|
||||
private readonly List<ICalendarItem> _allItems = new List<ICalendarItem>();
|
||||
|
||||
public CalendarEventCollection(ITimePeriod period, CalendarSettings settings)
|
||||
{
|
||||
Period = period;
|
||||
Settings = settings;
|
||||
|
||||
RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents);
|
||||
AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents);
|
||||
}
|
||||
|
||||
public bool HasCalendarEvent(AccountCalendar accountCalendar)
|
||||
=> _allItems.Any(x => x.AssignedCalendar.Id == accountCalendar.Id);
|
||||
|
||||
public ICalendarItem GetCalendarItem(Guid calendarItemId)
|
||||
{
|
||||
return _allItems.FirstOrDefault(x => x.Id == calendarItemId);
|
||||
}
|
||||
|
||||
public void ClearSelectionStates()
|
||||
{
|
||||
foreach (var item in _allItems)
|
||||
{
|
||||
if (item is ICalendarItemViewModel calendarItemViewModel)
|
||||
{
|
||||
calendarItemViewModel.IsSelected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void FilterByCalendars(IEnumerable<Guid> visibleCalendarIds)
|
||||
{
|
||||
foreach (var item in _allItems)
|
||||
{
|
||||
var collections = GetProperCollectionsForCalendarItem(item);
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
if (!visibleCalendarIds.Contains(item.AssignedCalendar.Id) && collection.Contains(item))
|
||||
{
|
||||
RemoveCalendarItemInternal(collection, item, false);
|
||||
}
|
||||
else if (visibleCalendarIds.Contains(item.AssignedCalendar.Id) && !collection.Contains(item))
|
||||
{
|
||||
AddCalendarItemInternal(collection, item, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<ObservableRangeCollection<ICalendarItem>> GetProperCollectionsForCalendarItem(ICalendarItem calendarItem)
|
||||
{
|
||||
// All-day events go to all days.
|
||||
// Multi-day events go to both.
|
||||
// Anything else goes to regular.
|
||||
|
||||
if (calendarItem.IsAllDayEvent)
|
||||
{
|
||||
return [_internalAllDayEvents];
|
||||
}
|
||||
else if (calendarItem.IsMultiDayEvent)
|
||||
{
|
||||
return [_internalRegularEvents, _internalAllDayEvents];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [_internalRegularEvents];
|
||||
}
|
||||
}
|
||||
|
||||
public void AddCalendarItem(ICalendarItem calendarItem)
|
||||
{
|
||||
var collections = GetProperCollectionsForCalendarItem(calendarItem);
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
AddCalendarItemInternal(collection, calendarItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveCalendarItem(ICalendarItem calendarItem)
|
||||
{
|
||||
var collections = GetProperCollectionsForCalendarItem(calendarItem);
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
RemoveCalendarItemInternal(collection, calendarItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveCalendarItems(Func<ICalendarItem, bool> predicate)
|
||||
{
|
||||
if (predicate == null) return;
|
||||
|
||||
var itemsToRemove = _allItems.Where(predicate).ToList();
|
||||
|
||||
foreach (var item in itemsToRemove)
|
||||
{
|
||||
RemoveCalendarItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
|
||||
{
|
||||
if (calendarItem is not ICalendarItemViewModel viewModel)
|
||||
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
|
||||
|
||||
// Set the displaying context for proper title calculation
|
||||
viewModel.DisplayingPeriod = Period;
|
||||
viewModel.CalendarSettings = Settings;
|
||||
|
||||
collection.Add(calendarItem);
|
||||
|
||||
if (create)
|
||||
{
|
||||
_allItems.Add(calendarItem);
|
||||
}
|
||||
|
||||
CalendarItemAdded?.Invoke(this, calendarItem);
|
||||
}
|
||||
|
||||
private void RemoveCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool destroy = true)
|
||||
{
|
||||
if (calendarItem is not ICalendarItemViewModel)
|
||||
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
|
||||
|
||||
collection.Remove(calendarItem);
|
||||
|
||||
if (destroy)
|
||||
{
|
||||
_allItems.Remove(calendarItem);
|
||||
}
|
||||
|
||||
CalendarItemRemoved?.Invoke(this, calendarItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing calendar item in-place. If the item's type changed (all-day vs regular),
|
||||
/// it will be moved to the appropriate collection.
|
||||
/// </summary>
|
||||
/// <param name="calendarItem">The updated calendar item data.</param>
|
||||
/// <returns>True if the item was found and updated; false otherwise.</returns>
|
||||
public bool UpdateCalendarItem(CalendarItem calendarItem)
|
||||
{
|
||||
var existingItem = _allItems.FirstOrDefault(x => x.Id == calendarItem.Id);
|
||||
if (existingItem == null)
|
||||
return false;
|
||||
|
||||
// Get the collections this item is currently in (before update)
|
||||
var oldCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
|
||||
|
||||
// Update the underlying data
|
||||
if (existingItem is ICalendarItemViewModel viewModel)
|
||||
{
|
||||
viewModel.UpdateFrom(calendarItem);
|
||||
}
|
||||
|
||||
// Get the collections this item should be in (after update)
|
||||
var newCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
|
||||
|
||||
// Check if the collections changed
|
||||
var collectionsToRemoveFrom = oldCollections.Except(newCollections).ToList();
|
||||
var collectionsToAddTo = newCollections.Except(oldCollections).ToList();
|
||||
|
||||
// Remove from old collections that are no longer applicable
|
||||
foreach (var collection in collectionsToRemoveFrom)
|
||||
{
|
||||
collection.Remove(existingItem);
|
||||
}
|
||||
|
||||
// Add to new collections that are now applicable
|
||||
foreach (var collection in collectionsToAddTo)
|
||||
{
|
||||
if (!collection.Contains(existingItem))
|
||||
{
|
||||
collection.Add(existingItem);
|
||||
}
|
||||
}
|
||||
|
||||
CalendarItemUpdated?.Invoke(this, existingItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_internalAllDayEvents.Clear();
|
||||
_internalRegularEvents.Clear();
|
||||
_allItems.Clear();
|
||||
|
||||
CalendarItemsCleared?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Core.Domain.Collections;
|
||||
|
||||
public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the range of dates that are currently displayed in the collection.
|
||||
/// </summary>
|
||||
public DateRange DisplayRange
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Count == 0) return null;
|
||||
|
||||
var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate;
|
||||
var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate;
|
||||
|
||||
return new DateRange(minimumLoadedDate, maximumLoadedDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveCalendarItem(ICalendarItem calendarItem)
|
||||
{
|
||||
foreach (var dayRange in this)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void AddCalendarItem(ICalendarItem calendarItem)
|
||||
{
|
||||
foreach (var dayRange in this)
|
||||
{
|
||||
var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start));
|
||||
calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Wino.Core.Domain.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class ObservableRangeCollection<T> : ObservableCollection<T>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
|
||||
/// </summary>
|
||||
public ObservableRangeCollection()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">collection: The collection from which the elements are copied.</param>
|
||||
/// <exception cref="ArgumentNullException">The collection parameter cannot be null.</exception>
|
||||
public ObservableRangeCollection(IEnumerable<T> collection)
|
||||
: base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
|
||||
/// </summary>
|
||||
public void AddRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
if (notificationMode != NotifyCollectionChangedAction.Add && notificationMode != NotifyCollectionChangedAction.Reset)
|
||||
throw new ArgumentException("Mode must be either Add or Reset for AddRange.", nameof(notificationMode));
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
CheckReentrancy();
|
||||
|
||||
var startIndex = Count;
|
||||
|
||||
var itemsAdded = AddArrangeCore(collection);
|
||||
|
||||
if (!itemsAdded)
|
||||
return;
|
||||
|
||||
if (notificationMode == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var changedItems = collection is List<T> ? (List<T>)collection : new List<T>(collection);
|
||||
|
||||
RaiseChangeNotificationEvents(
|
||||
action: NotifyCollectionChangedAction.Add,
|
||||
changedItems: changedItems,
|
||||
startingIndex: startIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). NOTE: with notificationMode = Remove, removed items starting index is not set because items are not guaranteed to be consecutive.
|
||||
/// </summary>
|
||||
public void RemoveRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
if (notificationMode != NotifyCollectionChangedAction.Remove && notificationMode != NotifyCollectionChangedAction.Reset)
|
||||
throw new ArgumentException("Mode must be either Remove or Reset for RemoveRange.", nameof(notificationMode));
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
CheckReentrancy();
|
||||
|
||||
if (notificationMode == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
var raiseEvents = false;
|
||||
foreach (var item in collection)
|
||||
{
|
||||
Items.Remove(item);
|
||||
raiseEvents = true;
|
||||
}
|
||||
|
||||
if (raiseEvents)
|
||||
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var changedItems = new List<T>(collection);
|
||||
for (var i = 0; i < changedItems.Count; i++)
|
||||
{
|
||||
if (!Items.Remove(changedItems[i]))
|
||||
{
|
||||
changedItems.RemoveAt(i); //Can't use a foreach because changedItems is intended to be (carefully) modified
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedItems.Count == 0)
|
||||
return;
|
||||
|
||||
RaiseChangeNotificationEvents(
|
||||
action: NotifyCollectionChangedAction.Remove,
|
||||
changedItems: changedItems);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current collection and replaces it with the specified item.
|
||||
/// </summary>
|
||||
public void Replace(T item) => ReplaceRange(new T[] { item });
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current collection and replaces it with the specified collection.
|
||||
/// </summary>
|
||||
public void ReplaceRange(IEnumerable<T> collection)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
CheckReentrancy();
|
||||
|
||||
var previouslyEmpty = Items.Count == 0;
|
||||
|
||||
Items.Clear();
|
||||
|
||||
AddArrangeCore(collection);
|
||||
|
||||
var currentlyEmpty = Items.Count == 0;
|
||||
|
||||
if (previouslyEmpty && currentlyEmpty)
|
||||
return;
|
||||
|
||||
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
|
||||
}
|
||||
|
||||
public void InsertRange(IEnumerable<T> items)
|
||||
{
|
||||
CheckReentrancy();
|
||||
|
||||
foreach (var item in items)
|
||||
Items.Insert(0, item);
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
private bool AddArrangeCore(IEnumerable<T> collection)
|
||||
{
|
||||
var itemAdded = false;
|
||||
foreach (var item in collection)
|
||||
{
|
||||
Items.Add(item);
|
||||
itemAdded = true;
|
||||
}
|
||||
return itemAdded;
|
||||
}
|
||||
|
||||
private void RaiseChangeNotificationEvents(NotifyCollectionChangedAction action, List<T> changedItems = null, int startingIndex = -1)
|
||||
{
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
|
||||
|
||||
if (changedItems is null)
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action));
|
||||
else
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, changedItems: changedItems, startingIndex: startingIndex));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,31 @@
|
||||
namespace Wino.Core.Domain
|
||||
namespace Wino.Core.Domain;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// MIME header that exists in all the drafts created from Wino.
|
||||
/// </summary>
|
||||
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
|
||||
public const string LocalDraftStartPrefix = "localDraft_";
|
||||
|
||||
public const string ToastMailItemIdKey = nameof(ToastMailItemIdKey);
|
||||
public const string CalendarEventRecurrenceRuleSeperator = "___";
|
||||
|
||||
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
|
||||
public const string ToastActionKey = nameof(ToastActionKey);
|
||||
}
|
||||
public const string ToastMailAccountIdKey = nameof(ToastMailAccountIdKey);
|
||||
public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey);
|
||||
public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey);
|
||||
public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction);
|
||||
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
|
||||
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
|
||||
public const string ToastModeKey = nameof(ToastModeKey);
|
||||
public const string ToastModeMail = nameof(ToastModeMail);
|
||||
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
||||
|
||||
public const string ClientLogFile = "Client_.log";
|
||||
public const string ServerLogFile = "Server_.log";
|
||||
public const string LogArchiveFileName = "WinoLogs.zip";
|
||||
|
||||
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
|
||||
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public class AccountSignature
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string HtmlBody { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using SQLite;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Back storage for simple name-address book.
|
||||
/// These values will be inserted during MIME fetch.
|
||||
/// </summary>
|
||||
|
||||
|
||||
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
|
||||
// Do it.
|
||||
public class AddressInformation : IEquatable<AddressInformation>
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Address { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string DisplayName => Address == Name ? Address : $"{Name} <{Address}>";
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return Equals(obj as AddressInformation);
|
||||
}
|
||||
|
||||
public bool Equals(AddressInformation other)
|
||||
{
|
||||
return !(other is null) &&
|
||||
Address == other.Address &&
|
||||
Name == other.Name;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int hashCode = -1717786383;
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Address);
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name);
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public static bool operator ==(AddressInformation left, AddressInformation right)
|
||||
{
|
||||
return EqualityComparer<AddressInformation>.Default.Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(AddressInformation left, AddressInformation right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
[Preserve]
|
||||
public class AccountCalendar : IAccountCalendar
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
public string RemoteCalendarId { get; set; }
|
||||
public string SynchronizationDeltaToken { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
public bool IsSynchronizationEnabled { get; set; } = true;
|
||||
public bool IsExtended { get; set; } = true;
|
||||
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
|
||||
|
||||
/// <summary>
|
||||
/// Unused for now.
|
||||
/// </summary>
|
||||
public string TextColorHex { get; set; }
|
||||
public string BackgroundColorHex { get; set; }
|
||||
public string TimeZone { get; set; }
|
||||
|
||||
[Ignore]
|
||||
public MailAccount MailAccount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Represents metadata for calendar event attachments.
|
||||
/// Actual file content is downloaded on-demand.
|
||||
/// </summary>
|
||||
public class CalendarAttachment
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The calendar item this attachment belongs to.
|
||||
/// </summary>
|
||||
public Guid CalendarItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Remote identifier for the attachment from the provider (Outlook, Gmail, etc.).
|
||||
/// </summary>
|
||||
public string RemoteAttachmentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File name of the attachment.
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the attachment in bytes.
|
||||
/// </summary>
|
||||
public long Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME content type (e.g., "application/pdf", "image/png").
|
||||
/// </summary>
|
||||
public string ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the attachment has been downloaded to local storage.
|
||||
/// </summary>
|
||||
public bool IsDownloaded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local file path where the attachment is stored (if downloaded).
|
||||
/// </summary>
|
||||
public string LocalFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the attachment was last modified.
|
||||
/// </summary>
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
// TODO: Connect to Contact store with Wino People.
|
||||
public class CalendarEventAttendee
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
public Guid CalendarItemId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public AttendeeStatus AttendenceStatus { get; set; }
|
||||
public bool IsOrganizer { get; set; }
|
||||
public bool IsOptionalAttendee { get; set; }
|
||||
public string Comment { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Itenso.TimePeriod;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
[DebuggerDisplay("{Title} ({StartDate} - {EndDate})")]
|
||||
public class CalendarItem : ICalendarItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
public string RemoteEventId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this item is a local preview that hasn't been synced to the server yet.
|
||||
/// When true, the item exists only in the local database without a RemoteEventId.
|
||||
/// Used to prevent duplicates when the server returns the newly created event.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public bool IsLocalPreview => string.IsNullOrEmpty(RemoteEventId);
|
||||
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
public DateTime EndDate
|
||||
{
|
||||
get
|
||||
{
|
||||
return StartDate.AddSeconds(DurationInSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IANA timezone identifier for the start time (e.g., "America/New_York", "Europe/London").
|
||||
/// If null or empty, UTC is assumed.
|
||||
/// </summary>
|
||||
public string StartTimeZone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IANA timezone identifier for the end time (e.g., "America/New_York", "Europe/London").
|
||||
/// If null or empty, UTC is assumed.
|
||||
/// </summary>
|
||||
public string EndTimeZone { get; set; }
|
||||
|
||||
private ITimePeriod _period;
|
||||
public ITimePeriod Period
|
||||
{
|
||||
get
|
||||
{
|
||||
_period ??= new TimeRange(StartDate, EndDate);
|
||||
|
||||
return _period;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Events that starts at midnight and ends at midnight are considered all-day events.
|
||||
/// </summary>
|
||||
public bool IsAllDayEvent
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
StartDate.TimeOfDay == TimeSpan.Zero &&
|
||||
EndDate.TimeOfDay == TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Events that are child instances of a recurring event (occurrences or exceptions).
|
||||
/// </summary>
|
||||
public bool IsRecurringChild
|
||||
{
|
||||
get
|
||||
{
|
||||
return RecurringCalendarItemId != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Events that are part of a recurring series (either as parent or child).
|
||||
/// </summary>
|
||||
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
|
||||
|
||||
/// <summary>
|
||||
/// Events that are the master event definition of recurrence events.
|
||||
/// </summary>
|
||||
public bool IsRecurringParent
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(Recurrence) && RecurringCalendarItemId == null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Events that are not all-day events and last more than one day are considered multi-day events.
|
||||
/// </summary>
|
||||
public bool IsMultiDayEvent
|
||||
{
|
||||
get
|
||||
{
|
||||
return Period.Duration.TotalDays >= 1 && !IsAllDayEvent;
|
||||
}
|
||||
}
|
||||
|
||||
public double DurationInSeconds { get; set; }
|
||||
public string Recurrence { get; set; }
|
||||
|
||||
public string OrganizerDisplayName { get; set; }
|
||||
public string OrganizerEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the parent calendar item of the recurring event.
|
||||
/// Exceptional instances are stored as a separate calendar item.
|
||||
/// This makes the calendar item a child of the recurring event.
|
||||
/// </summary>
|
||||
public Guid? RecurringCalendarItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates read-only events. Default is false.
|
||||
/// </summary>
|
||||
public bool IsLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hidden events must not be displayed to the user.
|
||||
/// This usually happens when a child instance of recurring parent is cancelled after creation.
|
||||
/// </summary>
|
||||
public bool IsHidden { get; set; }
|
||||
|
||||
// TODO
|
||||
public string CustomEventColorHex { get; set; }
|
||||
public string HtmlLink { get; set; }
|
||||
public DateTime? SnoozedUntil { get; set; }
|
||||
public CalendarItemStatus Status { get; set; }
|
||||
public CalendarItemVisibility Visibility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates how the event should be shown in the calendar (Free, Busy, Tentative, etc.).
|
||||
/// </summary>
|
||||
public CalendarItemShowAs ShowAs { get; set; } = CalendarItemShowAs.Busy;
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public Guid CalendarId { get; set; }
|
||||
|
||||
[Ignore]
|
||||
public IAccountCalendar AssignedCalendar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id to load information related to this event (attendees, reminders, etc.).
|
||||
/// For child events, if they have their own data, use their own Id.
|
||||
/// For events that share data with their parent, return parent's Id.
|
||||
/// </summary>
|
||||
public Guid EventTrackingId => Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start date converted to user's local timezone for display.
|
||||
/// StartDate is stored according to StartTimeZone.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public DateTime LocalStartDate
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.GetLocalStartDate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end date converted to user's local timezone for display.
|
||||
/// EndDate is calculated from StartDate and is in StartTimeZone.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public DateTime LocalEndDate
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.GetLocalEndDate();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings) => Period.ToString();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
public class Reminder
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
public Guid CalendarItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration in seconds before the event start time when the reminder should trigger.
|
||||
/// For example, 900 seconds = 15 minutes before event.
|
||||
/// </summary>
|
||||
public long DurationInSeconds { get; set; }
|
||||
public CalendarItemReminderType ReminderType { get; set; }
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public class CustomServerInformation
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
public string DisplayName { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string IncomingServer { get; set; }
|
||||
public string IncomingServerUsername { get; set; }
|
||||
public string IncomingServerPassword { get; set; }
|
||||
public string IncomingServerPort { get; set; }
|
||||
|
||||
public CustomIncomingServerType IncomingServerType { get; set; }
|
||||
|
||||
public string OutgoingServer { get; set; }
|
||||
public string OutgoingServerPort { get; set; }
|
||||
public string OutgoingServerUsername { get; set; }
|
||||
public string OutgoingServerPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// useSSL True: SslOnConnect
|
||||
/// useSSL False: StartTlsWhenAvailable
|
||||
/// </summary>
|
||||
|
||||
public ImapConnectionSecurity IncomingServerSocketOption { get; set; }
|
||||
public ImapAuthenticationMethod IncomingAuthenticationMethod { get; set; }
|
||||
|
||||
|
||||
public ImapConnectionSecurity OutgoingServerSocketOption { get; set; }
|
||||
public ImapAuthenticationMethod OutgoingAuthenticationMethod { get; set; }
|
||||
|
||||
public string ProxyServer { get; set; }
|
||||
public string ProxyServerPort { get; set; }
|
||||
|
||||
[Obsolete("As 1.7.0")]
|
||||
public bool IncomingRequiresSSL { get; set; }
|
||||
|
||||
[Obsolete("As 1.7.0")]
|
||||
public bool OutgoingRequresSSL { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
public class AccountSignature
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string HtmlBody { get; set; }
|
||||
|
||||
public Guid MailAccountId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
public class RemoteAccountAlias
|
||||
{
|
||||
/// <summary>
|
||||
/// Display address of the alias.
|
||||
/// </summary>
|
||||
public string AliasAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Address to be included in Reply-To header when alias is used for sending messages.
|
||||
/// </summary>
|
||||
public string ReplyToAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this alias is the primary alias for the account.
|
||||
/// </summary>
|
||||
public bool IsPrimary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the alias is verified by the server.
|
||||
/// Only Gmail aliases are verified for now.
|
||||
/// Non-verified alias messages might be rejected by SMTP server.
|
||||
/// </summary>
|
||||
public bool IsVerified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this alias is the root alias for the account.
|
||||
/// Root alias means the first alias that was created for the account.
|
||||
/// It can't be deleted or changed.
|
||||
/// </summary>
|
||||
public bool IsRootAlias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional sender name for the alias.
|
||||
/// Falls back to account's sender name if not set when preparing messages.
|
||||
/// Used for Gmail only.
|
||||
/// </summary>
|
||||
public string AliasSenderName { get; set; }
|
||||
}
|
||||
|
||||
public class MailAccountAlias : RemoteAccountAlias
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Id for the alias.
|
||||
/// </summary>
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account id that this alias is attached to.
|
||||
/// </summary>
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Root aliases can't be deleted.
|
||||
/// </summary>
|
||||
public bool CanDelete => !IsRootAlias;
|
||||
|
||||
public string SelectedSigningCertificateThumbprint { get; set; }
|
||||
public bool IsSmimeEncryptionEnabled { get; set; }
|
||||
|
||||
[Ignore]
|
||||
public X509Certificate2 SelectedSigningCertificate { get; set; }
|
||||
|
||||
[Ignore]
|
||||
public ObservableCollection<X509Certificate2> Certificates { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
/// <summary>
|
||||
/// Summary of the parsed MIME messages.
|
||||
/// Wino will do non-network operations on this table and others from the original MIME.
|
||||
/// </summary>
|
||||
public class MailCopy
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Id of the mail.
|
||||
/// </summary>
|
||||
[PrimaryKey]
|
||||
public Guid UniqueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Not unique id of the item. Some operations held on this Id, some on the UniqueId.
|
||||
/// Same message can be in different folder. In that case UniqueId is used.
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Folder that this mail belongs to.
|
||||
/// </summary>
|
||||
public Guid FolderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Conversation id for the mail.
|
||||
/// </summary>
|
||||
public string ThreadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME MessageId if exists.
|
||||
/// </summary>
|
||||
public string MessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// References header from MIME
|
||||
/// </summary>
|
||||
public string References { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In-Reply-To header from MIME
|
||||
/// </summary>
|
||||
public string InReplyTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name for the sender.
|
||||
/// </summary>
|
||||
public string FromName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Address of the sender.
|
||||
/// </summary>
|
||||
public string FromAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject of the mail.
|
||||
/// </summary>
|
||||
public string Subject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Short preview of the content.
|
||||
/// </summary>
|
||||
public string PreviewText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date that represents this mail has been created in provider servers.
|
||||
/// Stored always in UTC.
|
||||
/// </summary>
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Importance of the mail.
|
||||
/// </summary>
|
||||
public MailImportance Importance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Read status for the mail.
|
||||
/// </summary>
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag status.
|
||||
/// Flagged for Outlook.
|
||||
/// Important for Gmail.
|
||||
/// </summary>
|
||||
public bool IsFlagged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// To support Outlook.
|
||||
/// Gmail doesn't use it.
|
||||
/// </summary>
|
||||
public bool IsFocused { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether mail has attachments included or not.
|
||||
/// </summary>
|
||||
public bool HasAttachments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of mail item (regular mail, calendar invitation, calendar response, etc.).
|
||||
/// </summary>
|
||||
public MailItemType ItemType { get; set; } = MailItemType.Mail;
|
||||
|
||||
/// <summary>
|
||||
/// Assigned draft id.
|
||||
/// </summary>
|
||||
public string DraftId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mail is only created locally.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public bool IsLocalDraft => !string.IsNullOrEmpty(DraftId) && DraftId.StartsWith(Constants.LocalDraftStartPrefix);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this copy is draft or not.
|
||||
/// </summary>
|
||||
public bool IsDraft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File id that this mail is assigned to.
|
||||
/// This Id is immutable. It's used to find the file in the file system.
|
||||
/// Even after mapping local draft to remote draft, it will not change.
|
||||
/// </summary>
|
||||
public Guid FileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Folder that this mail is assigned to.
|
||||
/// Warning: This field is not populated by queries.
|
||||
/// Services or View Models are responsible for populating this field.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailItemFolder AssignedFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account that this mail is assigned to.
|
||||
/// Warning: This field is not populated by queries.
|
||||
/// Services or View Models are responsible for populating this field.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailAccount AssignedAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contact information of the sender if exists.
|
||||
/// Warning: This field is not populated by queries.
|
||||
/// Services or View Models are responsible for populating this field.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public AccountContact SenderContact { get; set; }
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => [UniqueId];
|
||||
public override string ToString() => $"{Subject} <-> {Id}";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
/// <summary>
|
||||
/// Maps a calendar invitation mail item to a persisted calendar event.
|
||||
/// </summary>
|
||||
public class MailInvitationCalendarMapping
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MailCopy.Id value of the invitation mail.
|
||||
/// </summary>
|
||||
public string MailCopyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// iCalendar UID extracted from invitation MIME/ICS content.
|
||||
/// </summary>
|
||||
public string InvitationUid { get; set; }
|
||||
|
||||
public Guid CalendarId { get; set; }
|
||||
public Guid CalendarItemId { get; set; }
|
||||
public string CalendarRemoteEventId { get; set; }
|
||||
|
||||
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
[DebuggerDisplay("{FolderName} - {SpecialFolderType}")]
|
||||
public class MailItemFolder : IMailItemFolder
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string RemoteFolderId { get; set; }
|
||||
public string ParentRemoteFolderId { get; set; }
|
||||
|
||||
public Guid MailAccountId { get; set; }
|
||||
public string FolderName { get; set; }
|
||||
public SpecialFolderType SpecialFolderType { get; set; }
|
||||
public bool IsSystemFolder { get; set; }
|
||||
public bool IsSticky { get; set; }
|
||||
public bool IsSynchronizationEnabled { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public bool ShowUnreadCount { get; set; }
|
||||
public DateTime? LastSynchronizedDate { get; set; }
|
||||
|
||||
// For IMAP
|
||||
public uint UidValidity { get; set; }
|
||||
public long HighestModeSeq { get; set; }
|
||||
public uint HighestKnownUid { get; set; }
|
||||
public DateTime? LastUidReconcileUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Outlook shares delta changes per-folder. Gmail is for per-account.
|
||||
/// This is only used for Outlook provider.
|
||||
/// </summary>
|
||||
public string DeltaToken { get; set; }
|
||||
|
||||
// For GMail Labels
|
||||
public string TextColorHex { get; set; }
|
||||
public string BackgroundColorHex { get; set; }
|
||||
|
||||
[Ignore]
|
||||
public List<IMailItemFolder> ChildFolders { get; set; } = [];
|
||||
|
||||
// Category and Move type folders are not valid move targets.
|
||||
// These folders are virtual. They don't exist on the server.
|
||||
public bool IsMoveTarget => !(SpecialFolderType == SpecialFolderType.More || SpecialFolderType == SpecialFolderType.Category);
|
||||
|
||||
public bool ContainsSpecialFolderType(SpecialFolderType type)
|
||||
{
|
||||
if (SpecialFolderType == type)
|
||||
return true;
|
||||
|
||||
foreach (var child in ChildFolders)
|
||||
{
|
||||
if (child.SpecialFolderType == type)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return child.ContainsSpecialFolderType(type);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static MailItemFolder CreateMoreFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.More, FolderName = Translator.MoreFolderNameOverride };
|
||||
public static MailItemFolder CreateCategoriesFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.Category, FolderName = Translator.CategoriesFolderNameOverride };
|
||||
|
||||
public override string ToString() => FolderName;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
public class MergedInbox
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public class MailAccount
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Given name of the account in Wino.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Display name of the authenticated user/account.
|
||||
/// API integrations will query this value from the API.
|
||||
/// IMAP is populated by user on setup dialog.
|
||||
/// </summary>
|
||||
|
||||
public string ProfileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account e-mail address.
|
||||
/// </summary>
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider type of the account. Outlook,Gmail etc...
|
||||
/// </summary>
|
||||
public MailProviderType ProviderType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For tracking change delta.
|
||||
/// Gmail : historyId
|
||||
/// Outlook: deltaToken
|
||||
/// </summary>
|
||||
public string SynchronizationDeltaIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the signature to be used for this account.
|
||||
/// Null if no signature should be used.
|
||||
/// </summary>
|
||||
public Guid? SignatureId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the account has any reason for an interactive user action to fix continue operating.
|
||||
/// </summary>
|
||||
public AccountAttentionReason AttentionReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id of the merged inbox this account belongs to.
|
||||
/// </summary>
|
||||
public Guid? MergedInboxId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the merged inbox this account belongs to.
|
||||
/// Ignored for all SQLite operations.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MergedInbox MergedInbox { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Populated only when account has custom server information.
|
||||
/// </summary>
|
||||
|
||||
[Ignore]
|
||||
public CustomServerInformation ServerInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account preferences.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailAccountPreferences Preferences { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public class MailAccountPreferences
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the account in MailAccount table.
|
||||
/// </summary>
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether sent draft messages should be appended to the sent folder.
|
||||
/// Some IMAP servers do this automatically, some don't.
|
||||
/// It's disabled by default.
|
||||
/// </summary>
|
||||
public bool ShouldAppendMessagesToSentFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the notifications are enabled for the account.
|
||||
/// </summary>
|
||||
public bool IsNotificationsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the custom account identifier color in hex.
|
||||
/// </summary>
|
||||
public string AccountColorHex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the account has Focused inbox support.
|
||||
/// Null if the account provider type doesn't support Focused inbox.
|
||||
/// </summary>
|
||||
public bool? IsFocusedInboxEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Summary of the parsed MIME messages.
|
||||
/// Wino will do non-network operations on this table and others from the original MIME.
|
||||
/// </summary>
|
||||
public class MailCopy : IMailItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Id of the mail.
|
||||
/// </summary>
|
||||
[PrimaryKey]
|
||||
public Guid UniqueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Not unique id of the item. Some operations held on this Id, some on the UniqueId.
|
||||
/// Same message can be in different folder. In that case UniqueId is used.
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Folder that this mail belongs to.
|
||||
/// </summary>
|
||||
public Guid FolderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Conversation id for the mail.
|
||||
/// </summary>
|
||||
public string ThreadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME MessageId if exists.
|
||||
/// </summary>
|
||||
public string MessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// References header from MIME
|
||||
/// </summary>
|
||||
public string References { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In-Reply-To header from MIME
|
||||
/// </summary>
|
||||
public string InReplyTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name for the sender.
|
||||
/// </summary>
|
||||
public string FromName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Address of the sender.
|
||||
/// </summary>
|
||||
public string FromAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject of the mail.
|
||||
/// </summary>
|
||||
public string Subject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Short preview of the content.
|
||||
/// </summary>
|
||||
public string PreviewText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date that represents this mail has been created in provider servers.
|
||||
/// Stored always in UTC.
|
||||
/// </summary>
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Importance of the mail.
|
||||
/// </summary>
|
||||
public MailImportance Importance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Read status for the mail.
|
||||
/// </summary>
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag status.
|
||||
/// Flagged for Outlook.
|
||||
/// Important for Gmail.
|
||||
/// </summary>
|
||||
public bool IsFlagged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// To support Outlook.
|
||||
/// Gmail doesn't use it.
|
||||
/// </summary>
|
||||
public bool IsFocused { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether mail has attachments included or not.
|
||||
/// </summary>
|
||||
public bool HasAttachments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Assigned draft id.
|
||||
/// </summary>
|
||||
public string DraftId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this copy is draft or not.
|
||||
/// </summary>
|
||||
public bool IsDraft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File id that this mail is assigned to.
|
||||
/// This Id is immutable. It's used to find the file in the file system.
|
||||
/// Even after mapping local draft to remote draft, it will not change.
|
||||
/// </summary>
|
||||
public Guid FileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Folder that this mail is assigned to.
|
||||
/// Warning: This field is not populated by queries.
|
||||
/// Services or View Models are responsible for populating this field.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailItemFolder AssignedFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account that this mail is assigned to.
|
||||
/// Warning: This field is not populated by queries.
|
||||
/// Services or View Models are responsible for populating this field.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailAccount AssignedAccount { get; set; }
|
||||
|
||||
public override string ToString() => $"{Subject} <-> {Id}";
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
[DebuggerDisplay("{FolderName} - {SpecialFolderType}")]
|
||||
public class MailItemFolder : IMailItemFolder
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string RemoteFolderId { get; set; }
|
||||
public string ParentRemoteFolderId { get; set; }
|
||||
|
||||
public Guid MailAccountId { get; set; }
|
||||
public string FolderName { get; set; }
|
||||
public SpecialFolderType SpecialFolderType { get; set; }
|
||||
public bool IsSystemFolder { get; set; }
|
||||
public bool IsSticky { get; set; }
|
||||
public bool IsSynchronizationEnabled { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public bool ShowUnreadCount { get; set; }
|
||||
public DateTime? LastSynchronizedDate { get; set; }
|
||||
|
||||
// For IMAP
|
||||
public uint UidValidity { get; set; }
|
||||
public long HighestModeSeq { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Outlook shares delta changes per-folder. Gmail is for per-account.
|
||||
/// This is only used for Outlook provider.
|
||||
/// </summary>
|
||||
public string DeltaToken { get; set; }
|
||||
|
||||
// For GMail Labels
|
||||
public string TextColorHex { get; set; }
|
||||
public string BackgroundColorHex { get; set; }
|
||||
|
||||
[Ignore]
|
||||
public List<IMailItemFolder> ChildFolders { get; set; } = [];
|
||||
|
||||
// Category and Move type folders are not valid move targets.
|
||||
// These folders are virtual. They don't exist on the server.
|
||||
public bool IsMoveTarget => !(SpecialFolderType == SpecialFolderType.More || SpecialFolderType == SpecialFolderType.Category);
|
||||
|
||||
public bool ContainsSpecialFolderType(SpecialFolderType type)
|
||||
{
|
||||
if (SpecialFolderType == type)
|
||||
return true;
|
||||
|
||||
foreach (var child in ChildFolders)
|
||||
{
|
||||
if (child.SpecialFolderType == type)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return child.ContainsSpecialFolderType(type);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString() => FolderName;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public class MergedInbox
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Back storage for simple name-address book.
|
||||
/// These values will be inserted during MIME fetch.
|
||||
/// </summary>
|
||||
|
||||
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
|
||||
// Do it.
|
||||
public class AccountContact : IEquatable<AccountContact>
|
||||
{
|
||||
/// <summary>
|
||||
/// E-mail address of the contact.
|
||||
/// </summary>
|
||||
[PrimaryKey]
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display name of the contact.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64 encoded profile image of the contact.
|
||||
/// </summary>
|
||||
public string Base64ContactPicture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All registered accounts have their contacts registered as root.
|
||||
/// Root contacts must not be overridden by any configuration.
|
||||
/// They are created on account creation.
|
||||
/// </summary>
|
||||
public bool IsRootContact { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, indicates that the contact has been manually modified by the user.
|
||||
/// Contacts with this flag set to true should not be updated during synchronization.
|
||||
/// </summary>
|
||||
public bool IsOverridden { get; set; } = false;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return Equals(obj as AccountContact);
|
||||
}
|
||||
|
||||
public bool Equals(AccountContact other)
|
||||
{
|
||||
return other is not null &&
|
||||
Address == other.Address &&
|
||||
Name == other.Name;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Address, Name);
|
||||
}
|
||||
|
||||
public static bool operator ==(AccountContact left, AccountContact right)
|
||||
{
|
||||
return EqualityComparer<AccountContact>.Default.Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(AccountContact left, AccountContact right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
public class CustomServerInformation
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This field is ignored. DisplayName is stored in MailAccount as SenderName from now.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public string DisplayName { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string IncomingServer { get; set; }
|
||||
public string IncomingServerUsername { get; set; }
|
||||
public string IncomingServerPassword { get; set; }
|
||||
public string IncomingServerPort { get; set; }
|
||||
|
||||
public CustomIncomingServerType IncomingServerType { get; set; }
|
||||
|
||||
public string OutgoingServer { get; set; }
|
||||
public string OutgoingServerPort { get; set; }
|
||||
public string OutgoingServerUsername { get; set; }
|
||||
public string OutgoingServerPassword { get; set; }
|
||||
|
||||
public string CalDavServiceUrl { get; set; }
|
||||
public string CalDavUsername { get; set; }
|
||||
public string CalDavPassword { get; set; }
|
||||
public ImapCalendarSupportMode CalendarSupportMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// useSSL True: SslOnConnect
|
||||
/// useSSL False: StartTlsWhenAvailable
|
||||
/// </summary>
|
||||
|
||||
public ImapConnectionSecurity IncomingServerSocketOption { get; set; }
|
||||
public ImapAuthenticationMethod IncomingAuthenticationMethod { get; set; }
|
||||
|
||||
|
||||
public ImapConnectionSecurity OutgoingServerSocketOption { get; set; }
|
||||
public ImapAuthenticationMethod OutgoingAuthenticationMethod { get; set; }
|
||||
|
||||
public string ProxyServer { get; set; }
|
||||
public string ProxyServerPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of concurrent clients that can connect to the server.
|
||||
/// Default is 5.
|
||||
/// </summary>
|
||||
public int MaxConcurrentClients { get; set; }
|
||||
|
||||
public Dictionary<string, string> GetConnectionProperties()
|
||||
{
|
||||
// Printout the public connection properties.
|
||||
|
||||
var connectionProperties = new Dictionary<string, string>
|
||||
{
|
||||
{ "IncomingServer", IncomingServer },
|
||||
{ "IncomingServerPort", IncomingServerPort },
|
||||
{ "IncomingServerSocketOption", IncomingServerSocketOption.ToString() },
|
||||
{ "IncomingAuthenticationMethod", IncomingAuthenticationMethod.ToString() },
|
||||
{ "OutgoingServer", OutgoingServer },
|
||||
{ "OutgoingServerPort", OutgoingServerPort },
|
||||
{ "OutgoingServerSocketOption", OutgoingServerSocketOption.ToString() },
|
||||
{ "OutgoingAuthenticationMethod", OutgoingAuthenticationMethod.ToString() },
|
||||
{ "CalendarSupportMode", CalendarSupportMode.ToString() },
|
||||
{ "CalDavServiceUrl", CalDavServiceUrl },
|
||||
{ "ProxyServer", ProxyServer },
|
||||
{ "ProxyServerPort", ProxyServerPort }
|
||||
};
|
||||
|
||||
return connectionProperties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user-defined keyboard shortcut for mail operations.
|
||||
/// </summary>
|
||||
public class KeyboardShortcut
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key combination string (e.g., "D", "Delete", "F1").
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The modifier keys for this shortcut.
|
||||
/// </summary>
|
||||
public ModifierKeys ModifierKeys { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The mail operation this shortcut triggers.
|
||||
/// </summary>
|
||||
public MailOperation MailOperation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this shortcut is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When this shortcut was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// User-friendly display name for the shortcut.
|
||||
/// </summary>
|
||||
public string DisplayName
|
||||
{
|
||||
get
|
||||
{
|
||||
var modifierText = string.Empty;
|
||||
if (ModifierKeys.HasFlag(ModifierKeys.Control))
|
||||
modifierText += "Ctrl+";
|
||||
if (ModifierKeys.HasFlag(ModifierKeys.Alt))
|
||||
modifierText += "Alt+";
|
||||
if (ModifierKeys.HasFlag(ModifierKeys.Shift))
|
||||
modifierText += "Shift+";
|
||||
if (ModifierKeys.HasFlag(ModifierKeys.Windows))
|
||||
modifierText += "Win+";
|
||||
|
||||
return modifierText + Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
public class MailAccount
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Given name of the account in Wino.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Display name of the authenticated user/account.
|
||||
/// API integrations will query this value from the API.
|
||||
/// IMAP is populated by user on setup dialog.
|
||||
/// </summary>
|
||||
|
||||
public string SenderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account e-mail address.
|
||||
/// </summary>
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider type of the account. Outlook,Gmail etc...
|
||||
/// </summary>
|
||||
public MailProviderType ProviderType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For tracking mail change delta.
|
||||
/// Gmail : historyId
|
||||
/// Outlook: deltaToken
|
||||
/// </summary>
|
||||
public string SynchronizationDeltaIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For tracking calendar change delta.
|
||||
/// Gmail: It's per-calendar, so unused.
|
||||
/// Outlook: deltaLink
|
||||
/// </summary>
|
||||
public string CalendarSynchronizationDeltaIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Gets or sets the custom account identifier color in hex.
|
||||
/// </summary>
|
||||
public string AccountColorHex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64 encoded profile picture of the account.
|
||||
/// </summary>
|
||||
public string Base64ProfilePictureData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the listing order of the account in the accounts list.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the account has any reason for an interactive user action to fix continue operating.
|
||||
/// </summary>
|
||||
public AccountAttentionReason AttentionReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id of the merged inbox this account belongs to.
|
||||
/// </summary>
|
||||
public Guid? MergedInboxId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the additional IMAP provider assignment for the account.
|
||||
/// Providers that use IMAP as a synchronizer but have special requirements.
|
||||
/// </summary>
|
||||
public SpecialImapProvider SpecialImapProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether calendar access is granted for this account.
|
||||
/// When false, synchronizers will not process EventMessages or calendar invitations.
|
||||
/// Default is false for existing accounts to prevent scope issues.
|
||||
/// New accounts created after this feature will have this set to true.
|
||||
/// </summary>
|
||||
public bool IsCalendarAccessGranted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the merged inbox this account belongs to.
|
||||
/// Ignored for all SQLite operations.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MergedInbox MergedInbox { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Populated only when account has custom server information.
|
||||
/// </summary>
|
||||
|
||||
[Ignore]
|
||||
public CustomServerInformation ServerInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account preferences.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailAccountPreferences Preferences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last time folder structure was synchronized.
|
||||
/// Used for optimization - skip folder sync if synced recently.
|
||||
/// </summary>
|
||||
public DateTime? LastFolderStructureSyncDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the account can perform ProfileInformation sync type.
|
||||
/// </summary>
|
||||
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Gmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the account can perform AliasInformation sync type.
|
||||
/// </summary>
|
||||
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
public class MailAccountPreferences
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the account in MailAccount table.
|
||||
/// </summary>
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether sent draft messages should be appended to the sent folder.
|
||||
/// Some IMAP servers do this automatically, some don't.
|
||||
/// It's disabled by default.
|
||||
/// </summary>
|
||||
public bool ShouldAppendMessagesToSentFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the notifications are enabled for the account.
|
||||
/// </summary>
|
||||
public bool IsNotificationsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the account has Focused inbox support.
|
||||
/// Null if the account provider type doesn't support Focused inbox.
|
||||
/// </summary>
|
||||
public bool? IsFocusedInboxEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether signature should be appended automatically.
|
||||
/// </summary>
|
||||
public bool IsSignatureEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this account's unread items should be included in taskbar badge.
|
||||
/// </summary>
|
||||
public bool IsTaskbarBadgeEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets signature for new messages. Null if signature is not needed.
|
||||
/// </summary>
|
||||
public Guid? SignatureIdForNewMessages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets signature for following messages. Null if signature is not needed.
|
||||
/// </summary>
|
||||
public Guid? SignatureIdForFollowingMessages { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
public class Thumbnail
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Domain { get; set; }
|
||||
|
||||
public string Gravatar { get; set; }
|
||||
public string Favicon { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public record SystemFolderConfiguration(MailItemFolder SentFolder,
|
||||
MailItemFolder DraftFolder,
|
||||
MailItemFolder ArchiveFolder,
|
||||
MailItemFolder TrashFolder,
|
||||
MailItemFolder JunkFolder);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Models.Authentication;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public class TokenInformation : TokenInformationBase
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
public string Address { get; set; }
|
||||
|
||||
public void RefreshTokens(TokenInformationBase tokenInformationBase)
|
||||
{
|
||||
if (tokenInformationBase == null)
|
||||
throw new ArgumentNullException(nameof(tokenInformationBase));
|
||||
|
||||
AccessToken = tokenInformationBase.AccessToken;
|
||||
RefreshToken = tokenInformationBase.RefreshToken;
|
||||
ExpiresAt = tokenInformationBase.ExpiresAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum AccountAttentionReason
|
||||
{
|
||||
public enum AccountAttentionReason
|
||||
{
|
||||
None,
|
||||
InvalidCredentials,
|
||||
MissingSystemFolderConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum AccountCacheResetReason
|
||||
{
|
||||
AccountRemoval,
|
||||
ExpiredCache
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum AccountCreationDialogState
|
||||
{
|
||||
public enum AccountCreationDialogState
|
||||
{
|
||||
Idle,
|
||||
SigningIn,
|
||||
PreparingFolders,
|
||||
CalendarMetadataFetch,
|
||||
Completed,
|
||||
ManuelSetupWaiting,
|
||||
TestingConnection,
|
||||
AutoDiscoverySetup,
|
||||
AutoDiscoveryInProgress
|
||||
}
|
||||
AutoDiscoveryInProgress,
|
||||
FetchingProfileInformation,
|
||||
Canceled,
|
||||
FetchingEvents
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the state of synchronizer.
|
||||
/// </summary>
|
||||
public enum AccountSynchronizerState
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the state of synchronizer.
|
||||
/// </summary>
|
||||
public enum AccountSynchronizerState
|
||||
{
|
||||
Idle,
|
||||
ExecutingRequests,
|
||||
Synchronizing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum AppLanguage
|
||||
{
|
||||
public enum AppLanguage
|
||||
{
|
||||
None,
|
||||
English,
|
||||
Deutsch,
|
||||
@@ -11,6 +11,10 @@
|
||||
Czech,
|
||||
Chinese,
|
||||
Spanish,
|
||||
French
|
||||
}
|
||||
French,
|
||||
Indonesian,
|
||||
Greek,
|
||||
PortugeseBrazil,
|
||||
Italian,
|
||||
Romanian
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum AppThemeType
|
||||
{
|
||||
public enum AppThemeType
|
||||
{
|
||||
System,
|
||||
PreDefined,
|
||||
Custom,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum ApplicationElementTheme
|
||||
{
|
||||
public enum ApplicationElementTheme
|
||||
{
|
||||
Default,
|
||||
Light,
|
||||
Dark
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum AttendeeStatus
|
||||
{
|
||||
NeedsAction,
|
||||
Accepted,
|
||||
Tentative,
|
||||
Declined
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum BackgroundSynchronizationReason
|
||||
{
|
||||
public enum BackgroundSynchronizationReason
|
||||
{
|
||||
SessionConnected,
|
||||
Timer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum CalendarDisplayType
|
||||
{
|
||||
Day,
|
||||
Week,
|
||||
WorkWeek,
|
||||
Month
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum CalendarEventTargetType
|
||||
{
|
||||
Single, // Show details for a single event.
|
||||
Series // Show the series event. Parent of all recurring events.
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Trigger to load more data.
|
||||
/// </summary>
|
||||
public enum CalendarInitInitiative
|
||||
{
|
||||
User,
|
||||
App
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum CalendarItemRecurrenceFrequency
|
||||
{
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Yearly
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum CalendarItemReminderType
|
||||
{
|
||||
Popup,
|
||||
Email
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Defines how a calendar item should be displayed in terms of availability.
|
||||
/// </summary>
|
||||
public enum CalendarItemShowAs
|
||||
{
|
||||
Free,
|
||||
Tentative,
|
||||
Busy,
|
||||
OutOfOffice,
|
||||
WorkingElsewhere
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum CalendarItemStatus
|
||||
{
|
||||
NotResponded,
|
||||
Accepted,
|
||||
Tentative,
|
||||
Cancelled,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the source of a calendar item update.
|
||||
/// </summary>
|
||||
public enum CalendarItemUpdateSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Update originated from client-side UI changes (ApplyUIChanges).
|
||||
/// </summary>
|
||||
ClientUpdated,
|
||||
|
||||
/// <summary>
|
||||
/// Update originated from client-side UI revert (RevertUIChanges).
|
||||
/// </summary>
|
||||
ClientReverted,
|
||||
|
||||
/// <summary>
|
||||
/// Update originated from server synchronization or database operations.
|
||||
/// </summary>
|
||||
Server
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum CalendarItemVisibility
|
||||
{
|
||||
Default,
|
||||
Public,
|
||||
Private,
|
||||
Confidential
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Which way in time to load more data for calendar.
|
||||
/// </summary>
|
||||
public enum CalendarLoadDirection
|
||||
{
|
||||
Replace,
|
||||
Previous,
|
||||
Next
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum CalendarOrientation
|
||||
{
|
||||
Horizontal,
|
||||
Vertical
|
||||
}
|
||||