Compare commits
637 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 |
@@ -8,6 +8,9 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
|||||||
tab_width = 4
|
tab_width = 4
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
end_of_line = crlf
|
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_coalesce_expression = true:suggestion
|
||||||
dotnet_style_null_propagation = true:suggestion
|
dotnet_style_null_propagation = true:suggestion
|
||||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = 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
|
# Code-block preferences
|
||||||
csharp_prefer_braces = true:silent
|
csharp_prefer_braces = true:silent
|
||||||
csharp_prefer_simple_using_statement = true:suggestion
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
csharp_style_namespace_declarations = block_scoped:silent
|
csharp_style_namespace_declarations = file_scoped:error
|
||||||
|
|
||||||
# Expression-level preferences
|
# Expression-level preferences
|
||||||
csharp_prefer_simple_default_expression = true:suggestion
|
csharp_prefer_simple_default_expression = true:suggestion
|
||||||
@@ -287,4 +290,6 @@ csharp_style_prefer_top_level_statements = true:silent
|
|||||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||||
csharp_style_prefer_readonly_struct = 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_conditional_expression_experimental = true:silent
|
||||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_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.props
|
||||||
*.nuget.targets
|
*.nuget.targets
|
||||||
|
|
||||||
# Nuget personal access tokens and Credentials
|
|
||||||
nuget.config
|
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
# Microsoft Azure Build Output
|
||||||
csx/
|
csx/
|
||||||
*.build.csdef
|
*.build.csdef
|
||||||
@@ -402,3 +399,4 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml
|
|||||||
Wino/obj/x86/Debug/XamlSaveStateFile.xml
|
Wino/obj/x86/Debug/XamlSaveStateFile.xml
|
||||||
*.cache
|
*.cache
|
||||||
.vs/Wino/v16/.suo
|
.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
|
**Min Version:** Windows 10 1809
|
||||||
**Target Version:** Windows 11 22H2
|
**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
|
## 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>
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// TODO: Handle with Translator, but it's not initialized here yet.
|
|
||||||
builder.AddText("Wino Mail is updated!");
|
|
||||||
builder.AddText(string.Format("New version {0} is ready.", 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,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Windows.ApplicationModel;
|
|
||||||
using Windows.ApplicationModel.Background;
|
|
||||||
|
|
||||||
namespace Wino.BackgroundTasks
|
|
||||||
{
|
|
||||||
public sealed class SessionConnectedTask : IBackgroundTask
|
|
||||||
{
|
|
||||||
public async void Run(IBackgroundTaskInstance taskInstance)
|
|
||||||
{
|
|
||||||
var def = taskInstance.GetDeferral();
|
|
||||||
|
|
||||||
// Run server on session connected by launching the Full Thrust process.
|
|
||||||
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
|
|
||||||
|
|
||||||
def.Complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +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|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,17 +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>
|
/// </summary>
|
||||||
/// MIME header that exists in all the drafts created from Wino.
|
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
|
||||||
/// </summary>
|
public const string LocalDraftStartPrefix = "localDraft_";
|
||||||
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
|
|
||||||
public const string LocalDraftStartPrefix = "localDraft_";
|
|
||||||
|
|
||||||
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
|
public const string CalendarEventRecurrenceRuleSeperator = "___";
|
||||||
public const string ToastActionKey = nameof(ToastActionKey);
|
|
||||||
|
|
||||||
public const string ClientLogFile = "Client_.log";
|
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
|
||||||
public const string ServerLogFile = "Server_.log";
|
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,17 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities
|
|
||||||
{
|
|
||||||
public class AccountSignature
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public string HtmlBody { get; set; }
|
|
||||||
|
|
||||||
public Guid MailAccountId { 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,53 +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; }
|
|
||||||
|
|
||||||
/// <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; }
|
|
||||||
|
|
||||||
/// <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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,82 +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 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 change delta.
|
|
||||||
/// Gmail : historyId
|
|
||||||
/// Outlook: deltaToken
|
|
||||||
/// </summary>
|
|
||||||
public string SynchronizationDeltaIdentifier { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// TODO: Gets or sets the custom account identifier color in hex.
|
|
||||||
/// </summary>
|
|
||||||
public string AccountColorHex { 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>
|
|
||||||
/// 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,49 +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 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 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
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 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; }
|
|
||||||
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };
|
|
||||||
public override string ToString() => $"{Subject} <-> {Id}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +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 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,30 +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; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unique object storage for authenticators if needed.
|
|
||||||
/// </summary>
|
|
||||||
public string UniqueId { 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,
|
||||||
None,
|
MissingSystemFolderConfiguration
|
||||||
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,
|
||||||
Idle,
|
PreparingFolders,
|
||||||
SigningIn,
|
CalendarMetadataFetch,
|
||||||
PreparingFolders,
|
Completed,
|
||||||
Completed,
|
ManuelSetupWaiting,
|
||||||
ManuelSetupWaiting,
|
TestingConnection,
|
||||||
TestingConnection,
|
AutoDiscoverySetup,
|
||||||
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>
|
Idle,
|
||||||
/// Indicates the state of synchronizer.
|
ExecutingRequests,
|
||||||
/// </summary>
|
Synchronizing
|
||||||
public enum AccountSynchronizerState
|
|
||||||
{
|
|
||||||
Idle,
|
|
||||||
ExecutingRequests,
|
|
||||||
Synchronizing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
namespace Wino.Core.Domain.Enums
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum AppLanguage
|
||||||
{
|
{
|
||||||
public enum AppLanguage
|
None,
|
||||||
{
|
English,
|
||||||
None,
|
Deutsch,
|
||||||
English,
|
Russian,
|
||||||
Deutsch,
|
Turkish,
|
||||||
Russian,
|
Polish,
|
||||||
Turkish,
|
Czech,
|
||||||
Polish,
|
Chinese,
|
||||||
Czech,
|
Spanish,
|
||||||
Chinese,
|
French,
|
||||||
Spanish,
|
Indonesian,
|
||||||
French,
|
Greek,
|
||||||
Indonesian,
|
PortugeseBrazil,
|
||||||
Greek,
|
Italian,
|
||||||
PortugeseBrazil,
|
Romanian
|
||||||
Italian
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace Wino.Core.Domain.Enums
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum AppThemeType
|
||||||
{
|
{
|
||||||
public enum AppThemeType
|
System,
|
||||||
{
|
PreDefined,
|
||||||
System,
|
Custom,
|
||||||
PreDefined,
|
|
||||||
Custom,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace Wino.Core.Domain.Enums
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum ApplicationElementTheme
|
||||||
{
|
{
|
||||||
public enum ApplicationElementTheme
|
Default,
|
||||||
{
|
Light,
|
||||||
Default,
|
Dark
|
||||||
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
|
||||||
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum CalendarSynchronizationType
|
||||||
|
{
|
||||||
|
ExecuteRequests, // Execute all requests in the queue.
|
||||||
|
CalendarMetadata, // Sync calendar metadata.
|
||||||
|
CalendarEvents, // Sync all events for all calendars.
|
||||||
|
SingleCalendar, // Sync events for only specified calendars.
|
||||||
|
UpdateProfile // Update profile information only.
|
||||||
|
}
|
||||||