510 Commits

Author SHA1 Message Date
Burak Kaan Köse 65f7e0236a Bump version. 2026-04-16 02:04:42 +02:00
Burak Kaan Köse e13aaadc78 Fix notification activation and calendar bootstrap flow 2026-04-16 01:32:48 +02:00
Burak Kaan Köse 94675eee9a Remove winui build workflow. 2026-04-15 19:57:00 +02:00
Burak Kaan Köse b3360ecd76 Merge configurable mail notification actions 2026-04-15 15:54:07 +02:00
Burak Kaan Köse 4ca26cb131 Add configurable mail notification actions 2026-04-15 15:43:07 +02:00
Burak Kaan Köse 7c0f8d4bb4 Handle Outlook accounts without aliases 2026-04-15 13:07:58 +02:00
Burak Kaan Köse 999d8cde73 Preserve Gmail user label casing 2026-04-15 12:16:03 +02:00
Burak Kaan Köse 1a1d69be56 New beta release workflow. 2026-04-15 04:24:04 +02:00
Burak Kaan Köse c2540926f4 Fix an issue with "CommunityToolkit.WinUI.Extensions" package was restored using labsFeed instead of Nuget. 2026-04-15 04:23:52 +02:00
Burak Kaan Köse 9424fd9a16 Testing package version. 2026-04-15 04:01:47 +02:00
Burak Kaan Köse 89b48d3ac4 Moer updates on beta release pipeline. 2026-04-15 04:01:25 +02:00
Burak Kaan Köse 0bcc7a7647 Use existing version from the branch. 2026-04-15 03:37:47 +02:00
Burak Kaan Köse 260e1ab935 RID issue fix. 2026-04-15 03:17:34 +02:00
Burak Kaan Köse ccf7c0607b Remove cert password. 2026-04-15 03:06:18 +02:00
Burak Kaan Köse b8ce7e7422 Fix the incorrect feed for community toolkit extension thing. 2026-04-15 02:52:38 +02:00
Burak Kaan Köse 1365e42fd7 Explicit nuget.config for the workflow. 2026-04-15 02:46:06 +02:00
Burak Kaan Köse 0f160545ab Changelog. 2026-04-15 02:26:54 +02:00
Burak Kaan Köse 8481a5c7cd Better changelog handling for beta release workflow. 2026-04-15 02:24:11 +02:00
Burak Kaan Köse d32745fd67 Merge branch 'codex/manual-beta-release' 2026-04-15 02:14:52 +02:00
Burak Kaan Köse 470b2b8638 Add manual beta release workflow 2026-04-15 02:14:44 +02:00
Burak Kaan Köse 7e1731f4dc Fix compose initial focus behavior 2026-04-15 02:12:01 +02:00
Burak Kaan Köse aac9f9fec3 Merge branch 'codex/mail-categories-v1' 2026-04-15 01:18:12 +02:00
Burak Kaan Köse cf8fff8ef1 Add mail categories support 2026-04-15 01:18:07 +02:00
Burak Kaan Köse 0610096b78 Handle read-only calendars 2026-04-14 17:52:38 +02:00
Burak Kaan Köse feff929333 Bump version 2.0.1 - April 14. 2026-04-14 01:54:51 +02:00
Burak Kaan Köse aa16609f89 Add Windows share target draft attachment flow 2026-04-14 01:23:59 +02:00
Burak Kaan Köse 4bea53a667 Add custom theme deletion flow 2026-04-14 01:00:21 +02:00
Burak Kaan Köse b2ad4a1664 Fixing the Ui for reader & composer page. 2026-04-14 00:36:35 +02:00
Burak Kaan Köse dad3a51885 Remove debug print on preferences service. 2026-04-14 00:29:39 +02:00
Burak Kaan Köse 59ff0a1d7d Fix focused inbox not updating. 2026-04-14 00:29:29 +02:00
Burak Kaan Köse df19ab3196 Self contained. 2026-04-14 00:11:26 +02:00
Burak Kaan Köse c622858d2d Add initial mail sync range selection 2026-04-14 00:03:58 +02:00
Burak Kaan Köse 2e36772a4c Imap setup simplified and fixed the threading issues. 2026-04-13 23:11:35 +02:00
Burak Kaan Köse a2acad9ea4 Fix s/mime vertical combo on aliases. 2026-04-13 01:17:56 +02:00
Burak Kaan Köse 40b15b4f08 Improve alias capability model and Outlook alias sync 2026-04-13 01:09:45 +02:00
Burak Kaan Köse 6fd66810e9 Add email templates back to the settings menu. 2026-04-13 01:08:44 +02:00
Burak Kaan Köse 758a186c26 Remove test notification from event details. 2026-04-12 20:53:07 +02:00
Burak Kaan Köse b6bf5f2cd1 Remove new contact fromm menu list. 2026-04-12 20:12:48 +02:00
Burak Kaan Köse 3977401057 join online notification resolver fix. 2026-04-12 19:42:50 +02:00
Burak Kaan Köse 10c797fba4 Remove progress ring from accoount menu item 2026-04-12 19:42:27 +02:00
Burak Kaan Köse d922dd2f2e Fix online search dedupe and pane layout scrolling 2026-04-12 15:56:31 +02:00
Burak Kaan Köse 4d04595d0a Somem Ui issues. 2026-04-12 15:55:40 +02:00
Burak Kaan Köse 4ac3ca3ee4 Default open pane length size. 2026-04-12 01:53:37 +02:00
Burak Kaan Köse 678245d1fa Small UI adjustment for account menu progress. 2026-04-12 01:38:18 +02:00
Burak Kaan Köse c8ab214651 Merge read receipt tracking work 2026-04-11 21:03:22 +02:00
Burak Kaan Köse 230039cb57 Add read receipt tracking for sent mail 2026-04-11 21:02:51 +02:00
Burak Kaan Köse 448ebd6036 Respect API calendar colors unless overridden 2026-04-11 15:18:35 +02:00
Burak Kaan Köse e206368801 Migrate mail printing to WinUI print preview 2026-04-11 15:07:25 +02:00
Burak Kaan Köse 24626d1c31 Fix AppNotification multi-entry notifications 2026-04-11 13:02:44 +02:00
Burak Kaan Köse 5cb49efeb4 Updated synchronization progress implementation. 2026-04-11 13:00:56 +02:00
Burak Kaan Köse 40318ef99c Optimize mail list selection and draft pruning 2026-04-11 10:54:21 +02:00
Burak Kaan Köse fdb340549d Restore dual mail and calendar app entries 2026-04-11 01:28:25 +02:00
Burak Kaan Köse 4cb08f0a98 Implemented initial version for popping out window for rendering and compose pages 2026-04-11 01:05:08 +02:00
Burak Kaan Köse d5c121ce24 Fixing missing immamges from MSIX bundle. 2026-04-10 19:39:56 +02:00
Burak Kaan Köse be71ef3611 Fix packaging errors and revert test code. 2026-04-10 14:50:53 +02:00
Burak Kaan Köse 2a0d15ad69 Fixing notification activations. 2026-04-09 16:08:33 +02:00
Burak Kaan Köse a8310f1dab Fixing the leak in calendar mmode. 2026-04-09 01:26:23 +02:00
Burak Kaan Köse aaf0b7d069 Better creation of context menu items for calendar events. 2026-04-09 01:01:28 +02:00
Burak Kaan Köse 832a4b0348 Fixing couple issues with context flyout and moving items. 2026-04-09 00:17:30 +02:00
Burak Kaan Köse d6049bbd88 Calendar item context flyout implementation 2026-04-08 23:48:17 +02:00
Burak Kaan Köse 3dc4ac03ec Initial feature for drag / drop calendar events. 2026-04-08 23:46:02 +02:00
Burak Kaan Köse a3c35dfae5 Fixed an issue where updated outlook events turn into text instead of html. 2026-04-08 19:52:01 +02:00
Burak Kaan Köse cbdcfeae05 Missing translations. 2026-04-08 15:46:24 +02:00
Burak Kaan Köse 58a4e677e4 Agents.md update 2026-04-08 15:46:15 +02:00
Burak Kaan Köse 76f6ae0a1e - Fix for gmail calendar event creation.
- Proper junk API calls for gmail and outlook, not just moving the item.
- Add ability to hide ai actions panel.
2026-04-08 15:31:14 +02:00
Burak Kaan Köse a855d8c8a8 Gmail calendar actions fix. 2026-04-08 13:09:04 +02:00
Burak Kaan Köse 1567d9fa5e Fix some rendering issues in calendar with all day events. 2026-04-07 21:49:23 +02:00
Burak Kaan Köse 71fc883e47 Immidiate ui reflection for calendar events and some more error handling. 2026-04-07 16:48:46 +02:00
Burak Kaan Köse 3db54023a4 better 404 handling. 2026-04-07 13:23:07 +02:00
Burak Kaan Köse a9fd624742 2 second webview cache. 2026-04-07 09:52:37 +02:00
Burak Kaan Köse 9855170b2e Fixing issues with replies. 2026-04-07 01:17:52 +02:00
Burak Kaan Köse 12acff3bf8 Allow unsafe. 2026-04-07 00:25:16 +02:00
Burak Kaan Köse f693299304 Get rid of system.drawing and uwp notifications pacakge. Remove the AOT/trim stuff for now. 2026-04-07 00:02:36 +02:00
Burak Kaan Köse ff05195416 Better thread handling for mail collection. 2026-04-06 11:21:51 +02:00
Burak Kaan Köse c8265e75be Remove paddle billing callbacks. 2026-04-06 00:27:57 +02:00
Burak Kaan Köse 81e476e699 Gmail calendar sync fix. 2026-04-06 00:27:36 +02:00
Burak Kaan Köse 3357f6273c Public wino api service. 2026-04-05 16:56:32 +02:00
Burak Kaan Köse 323bbf7ea3 Resolving conflicts. 2026-04-05 16:53:51 +02:00
Burak Kaan Köse ef85ce6947 Merged feature/vNext. Initial commit for Wino Mail 2.0 2026-04-05 16:30:26 +02:00
Burak Kaan Köse 748ac8377a Bulk translation script usage. 2026-04-05 16:25:48 +02:00
Burak Kaan Köse 08bb9ede2a AI translations. 2026-04-05 16:23:20 +02:00
Burak Kaan Köse 32d677025d Cleanup old version service for maintenance. 2026-04-05 15:19:14 +02:00
Burak Kaan Köse ac78cf2b78 Remove GitHub Packages setup section from README
Removed GitHub Packages setup instructions from README.
2026-04-05 15:08:51 +02:00
Burak Kaan Köse 5f64cca518 Publich contracts package. 2026-04-05 14:14:13 +02:00
Burak Kaan Köse ca19297b92 Some more cleanup. 2026-04-05 13:18:50 +02:00
Burak Kaan Köse c1ab49fb1d Snooze with background activation. 2026-04-05 02:19:11 +02:00
Burak Kaan Köse 6013865fca Fix build error. 2026-04-04 20:34:55 +02:00
Burak Kaan Köse 1d0fcfb5b0 Import functionality for wino accounts, calendar sync UI, bunch of shell improvements 2026-04-04 20:23:20 +02:00
Burak Kaan Köse 1667aa34db AI action panel improvements. 2026-04-04 01:34:57 +02:00
Burak Kaan Köse 1211e9b28a New AI actions panel. Replaced new command bar. 2026-04-03 19:50:52 +02:00
Burak Kaan Köse 27e91316d3 Translation caching. New ai actions panel. 2026-04-03 11:56:25 +02:00
Burak Kaan Köse 8f16f553f5 Handling of AI pack through mmicrosoft store. 2026-04-02 15:07:05 +02:00
Burak Kaan Köse 7b369201b0 General account details settings and some marking mail issues 2026-04-01 01:41:17 +02:00
Burak Kaan Köse 6f61605c12 Handling of all day events and auto calendar sync on account creation. 2026-03-28 01:44:12 +01:00
Burak Kaan Köse 686446937b Disable aot for tests. 2026-03-27 21:00:02 +01:00
Burak Kaan Köse fb8a3d8f90 Handling some warnings and proper disposals of shells etc. 2026-03-27 14:45:36 +01:00
Burak Kaan Köse 3712041689 Some configs. 2026-03-27 12:58:41 +01:00
Burak Kaan Köse 37afb990f1 Better top shell account icon. 2026-03-27 12:58:25 +01:00
Burak Kaan Köse a465545fcb Live url 2026-03-27 12:58:08 +01:00
Burak Kaan Köse 022ffc567b AOT compatible signature and encryption page. 2026-03-27 12:57:44 +01:00
Burak Kaan Köse e3c3b341e5 Calendar improvements cycle 2 2026-03-25 15:49:14 +01:00
Burak Kaan Köse 8c492bb094 Calendar rendering improvements. 2026-03-25 13:39:27 +01:00
Burak Kaan Köse 0056f372b9 Fix search and global title bar issues. 2026-03-25 09:45:49 +01:00
Burak Kaan Köse 7aad6b0157 Wiring up the AI calls. 2026-03-25 00:25:05 +01:00
Burak Kaan Köse fd81ee31ce Tests 2026-03-24 23:20:41 +01:00
Burak Kaan Köse 27c90d2f89 Back navigation and shell improvements. 2026-03-24 18:05:09 +01:00
Burak Kaan Köse d699818c6f ShellContent improvements. 2026-03-24 16:57:13 +01:00
Burak Kaan Köse 317cad2459 Ai contracts update and special nav item for wino accounts. 2026-03-24 01:34:54 +01:00
Burak Kaan Köse ff84d62196 Settings refactoring. 2026-03-24 01:18:06 +01:00
Burak Kaan Köse 5c7f6aa734 Good improvements on the calendar. 2026-03-23 23:31:26 +01:00
Burak Kaan Köse 1adba271e2 Calendar rendering implementation. 2026-03-23 14:56:36 +01:00
Burak Kaan Köse 8586d0ef54 Calendar rendering. 2026-03-23 10:22:47 +01:00
Burak Kaan Köse 8d143e3b08 Remove old shells, some UI improvements for settings. 2026-03-21 18:25:54 +01:00
Burak Kaan Köse e6a38a3e77 New default style for calendar view. 2026-03-21 10:12:00 +01:00
Burak Kaan Köse 51fef043ee Range thing. 2026-03-21 00:58:01 +01:00
Burak Kaan Köse 01f7a09cb7 Merged 2026-03-20 13:26:45 +01:00
Burak Kaan Köse eb8cd7651d Calendar buttons etc. 2026-03-20 13:26:16 +01:00
Burak Kaan Köse c88c875fb8 Redesign Wino Account flyout with hero header and benefit cards 2026-03-20 13:12:11 +01:00
Claude a00af1da3f Redesign Wino Account flyout menu with Windows 11 Fluent Design
Replaces the plain signed-out flyout with a polished layout featuring:
- Gradient hero header with account icon and descriptive text
- Two benefit cards (sync settings, unlock add-ons) matching dialog patterns
- Equal-width Sign In (accent) and Register buttons
- Redesigned signed-in view with gradient profile header and larger avatar
- New translation keys for flyout benefit card content

https://claude.ai/code/session_019YP6uNe52Wo3SMuqz6w8QE
2026-03-20 12:10:15 +00:00
Burak Kaan Köse 1fe569e0ac Native tray icon implementation. 2026-03-20 12:43:09 +01:00
Burak Kaan Köse 4a20ea2577 Better profile caching. 2026-03-20 00:15:10 +01:00
Burak Kaan Köse d38317f0be Point real api. 2026-03-19 16:42:36 +01:00
Burak Kaan Köse c2320de5c4 Forgot password and email confirmations. 2026-03-19 16:41:35 +01:00
Burak Kaan Köse 873a7eca12 Bump contracts. 2026-03-19 14:32:12 +01:00
Burak Kaan Köse c3e1991942 Deep link on purchase success. 2026-03-19 10:26:17 +01:00
Burak Kaan Köse b0ee5c9974 Handling of paddle purchases and add-ons. 2026-03-19 01:50:14 +01:00
Burak Kaan Köse f306f6eb1c More updates on wino acc. 2026-03-18 17:43:56 +01:00
Burak Kaan Köse a3b43fd079 Add privacy policy for wino accounts when registering. 2026-03-18 14:45:00 +01:00
Burak Kaan Köse bac291587d New management page. 2026-03-18 10:25:07 +01:00
Burak Kaan Köse aee32228c2 Wino accounts settings. 2026-03-18 09:00:26 +01:00
Burak Kaan Köse 0d6da30a29 Bump contracts. 2026-03-17 17:48:47 +01:00
Burak Kaan Köse 289d0c8eeb Replace bland "W" initials with accent-colored person icon for signed-out state (#836)
When no Wino account is logged in, the titlebar button now shows a
filled accent-blue circle with a white person silhouette icon instead
of a PersonPicture with "W" initials. This makes the button visually
prominent and clearly communicates it's an account action. When signed
in, the PersonPicture with user initials is shown as before.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:47:45 +01:00
Burak Kaan Köse 4a94dfb10c Redesign Wino Account login and registration dialogs (#835)
Add hero illustrations using XAML-native vector graphics (shield+lock
for login, person+plus badge for registration). Improve visual design
with gradient backgrounds, icon badges on benefit cards, and refined
spacing/corner radii. Add Enter key handling so pressing Enter in an
input field moves focus to the next field, and pressing Enter on the
last field triggers the primary action (login/register).

https://claude.ai/code/session_011B1M6UVeo4yUX3zNHBxQ2P

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-17 16:08:04 +01:00
Burak Kaan Köse ea204fef21 Add preference to hide title bar Wino account button (#833) 2026-03-17 12:00:34 +01:00
Burak Kaan Köse e2b9216f8f New base url. 2026-03-17 02:00:53 +01:00
Burak Kaan Köse 5f519f6ae1 Fixing back navigation. 2026-03-16 21:41:22 +01:00
Burak Kaan Köse 5b2a99ffe5 Dialog improvements. 2026-03-16 15:02:39 +01:00
Burak Kaan Köse 4519b77444 Document GitHub Packages setup 2026-03-16 13:32:59 +01:00
Burak Kaan Köse 59d9cf4eea Consume Wino.Mail.Contracts package 2026-03-16 12:12:13 +01:00
Burak Kaan Köse 0ee3a0c3bc Delete instructions. 2026-03-16 11:23:13 +01:00
Burak Kaan Köse 37c1bd3f62 Sign in , out ,register. 2026-03-16 01:33:27 +01:00
Burak Kaan Köse 921c3bef93 Some ui adjustments on settings. 2026-03-14 22:37:54 +01:00
Burak Kaan Köse 45142e6953 Settings home refactoring. 2026-03-14 21:03:52 +01:00
Burak Kaan Köse 642f6efbfb Fix active canvas null issue. 2026-03-14 18:25:28 +01:00
Burak Kaan Köse 56b0f79edc navigation improvements 2026-03-14 14:14:58 +01:00
Burak Kaan Köse 4ba7d5fd07 Fixing menu item scrolling issue with nav bar. 2026-03-13 07:20:37 +01:00
Burak Kaan Köse 7f0b671b62 Settings shell. 2026-03-12 19:04:47 +01:00
Burak Kaan Köse de5309ea56 Finalized new shell experience. 2026-03-12 14:55:07 +01:00
Burak Kaan Köse fd13f2eba5 Calendar crashes fix. 2026-03-12 12:10:23 +01:00
Burak Kaan Köse 861b991eee Shell improvements. 2026-03-12 11:28:41 +01:00
Burak Kaan Köse 9dd68fd62e Improved shell experience. 2026-03-11 19:26:37 +01:00
Burak Kaan Köse 2b523d64e8 New shell experience. 2026-03-11 01:39:32 +01:00
Burak Kaan Köse bf331dfeb3 Better shell 2026-03-10 16:50:16 +01:00
Burak Kaan Köse 9b567c4bac Go back to welcome page when the last account is removed. 2026-03-09 14:18:13 +01:00
Burak Kaan Köse 859a5bb117 Revise README with new logo and updated text
Updated logo and description in README.md, removed unused images.
2026-03-09 13:19:12 +01:00
Burak Kaan Köse 0d898d3de0 Remove website visual assets section from README (#830) 2026-03-09 13:15:42 +01:00
Burak Kaan Köse 44be3eb4f7 Some UI changes on settings. 2026-03-09 00:28:10 +01:00
Burak Kaan Köse 3e731967cd Remove edit account details page. 2026-03-08 21:21:34 +01:00
Burak Kaan Köse 8548257878 Fix udate update notes. 2026-03-08 18:40:43 +01:00
Burak Kaan Köse d9da326f0a Rename database. 2026-03-08 16:28:41 +01:00
Burak Kaan Köse d43e2b269a Fix tailored notification images issue. 2026-03-08 16:26:06 +01:00
Burak Kaan Köse 9d94badb95 Fix storage page navigation issue. 2026-03-08 16:25:53 +01:00
Burak Kaan Köse e4a224bd68 Emaıl templates. 2026-03-08 15:48:11 +01:00
Burak Kaan Köse 15400d4096 Improved keyboad shortcuts. 2026-03-08 13:21:42 +01:00
Burak Kaan Köse c1568d33e6 Live store update notifications. 2026-03-08 11:22:41 +01:00
Burak Kaan Köse a8f9b2d126 Calendar improvements. 2026-03-08 01:33:47 +01:00
Burak Kaan Köse 1da34080d1 Refactoring the html editor toolbar. 2026-03-07 23:33:25 +01:00
Burak Kaan Köse ebc35c3de8 Event creation. 2026-03-07 17:13:48 +01:00
Burak Kaan Köse d1f8163d72 Web editor refactoring and some calendar occurrence summary stuff. 2026-03-07 15:37:02 +01:00
Burak Kaan Köse 09f1cee3a5 Remove sqlite base64 contact store from AccountContact. 2026-03-07 11:43:56 +01:00
Burak Kaan Köse 8e8b123aa6 Updating some nugets. 2026-03-07 01:52:23 +01:00
Burak Kaan Köse 9ec7b32762 Merge branch 'feature/EventCompose' into feature/vNext 2026-03-07 01:46:21 +01:00
Burak Kaan Köse e94cce451f Event compose implementation. 2026-03-07 01:46:07 +01:00
Burak Kaan Köse 6608baed69 Initial event composing. 2026-03-06 17:46:38 +01:00
Burak Kaan Köse 59042729c1 "Outlook error" handling for 429 status code. 2026-03-06 13:43:16 +01:00
Burak Kaan Köse e1be644631 Contact and settings updates. 2026-03-06 12:31:37 +01:00
Burak Kaan Köse 51f64466c2 Updating core title bar for menu item changes. 2026-03-06 11:22:19 +01:00
Burak Kaan Köse 24f7c26d60 Visual refresh of dialogs. 2026-03-06 11:22:12 +01:00
Burak Kaan Köse 1aaf4e8a7e Settings UI 2026-03-06 04:04:14 +01:00
Burak Kaan Köse 3d6763770e merged 2026-03-06 03:43:06 +01:00
Burak Kaan Köse aaa6e8a2c9 Removed migrations. New onboarding screen and wizard like steps. 2026-03-06 03:42:08 +01:00
Burak Kaan Köse db5ecd60e4 New startup window. 2026-03-05 10:12:03 +01:00
Burak Kaan Köse 5b3739c6cf Add snooze support for calendar reminders (toast UI, service, DB) (#825)
* Filter reminder snooze options by default reminder

* Some updates.

* Fixing empty welcome page issue and attendee loading.

* Icon system for notifications and snooze options etc.
2026-03-04 00:12:52 +01:00
Burak Kaan Köse d45d3faa89 Whats new implementation. 2026-03-02 00:44:29 +01:00
Burak Kaan Köse e816e87f61 Contacts management. 2026-03-01 21:07:10 +01:00
Burak Kaan Köse bdd32786d6 folder structure fixes 2026-03-01 16:23:28 +01:00
Burak Kaan Köse f35a4333f9 Fix initials showing in the background when the profile picture has transparent background. 2026-03-01 13:48:40 +01:00
Burak Kaan Köse 2c9351f551 Fixing some IsBusy corner cases. 2026-03-01 12:40:12 +01:00
Burak Kaan Köse 211faff750 Property change based updates on the mails for fast bulk operations. 2026-03-01 12:07:15 +01:00
Burak Kaan Köse 11158fe737 Remove redundant notification target. 2026-03-01 09:50:05 +01:00
Burak Kaan Köse 76e3b7289e Some issues with changing the app mode and notifications have been fixed. 2026-03-01 09:47:05 +01:00
Burak Kaan Köse 2040d4abce Optimize mail fetching with batch DB queries and in-memory caching (#827)
* perf: batch-load folders, accounts, and contacts in FetchMailsAsync

Replace the sequential per-mail property-loading loop with a three-step
batch pre-load strategy, eliminating the N+1 DB call pattern that was
the main bottleneck when building the mail list with threading enabled.

Changes:
- Pre-seed the folder cache from MailListInitializationOptions.Folders
  so that the most common folders (inbox, sent, etc.) never trigger a DB
  lookup at all.
- Load all accounts in a single GetAccountsAsync() call instead of one
  GetAccountAsync() call per mail (typically 1–5 accounts total).
- Fetch all sender contacts in a single SQL IN(...) query via the new
  GetContactsByAddressesAsync() method instead of one query per address.
- Property assignment is now fully synchronous (no awaits in the loop)
  since all data is pre-loaded into plain Dictionary<K,V>.
- Thread-expansion follows the same pattern: new folder IDs are loaded
  in parallel via Task.WhenAll; new contact addresses are batch-fetched
  with a second IN(...) query.
- Also apply batch pre-loading to GetMailItemsAsync (used by merge-inbox
  sync path) which had the same sequential issue.
- Remove the now-unused LoadAssignedPropertiesWithCacheAsync helper and
  the ConcurrentDictionary dependency it required.
- Tighten GetMailsByThreadIdsAsync to skip the Id NOT IN clause entirely
  when the exclusion set is empty.

https://claude.ai/code/session_018bqahGc6zi95JJhc2aARKS

* test: add MailFetchingTests with correctness and performance coverage

Adds integration tests for MailService.FetchMailsAsync that exercise the
full real-service stack (MailService → FolderService / AccountService /
ContactService) backed by the shared in-memory SQLite helper.

Four tests are included:

• ExpandsSiblingsOutsidePage – proves thread expansion fetches mails that
  fall beyond the initial SQL page (6 mails, page=4, expects 6 returned).

• NeverExpandsSiblings – proves threading is truly opt-in; with
  CreateThreads=false the result exactly matches the raw page size.

• ResolvesFromAllThreeSources – verifies contact resolution for a known
  contact (from the AccountContact table), an unknown sender (ad-hoc
  fallback), and a self-sent mail (built from account metadata).

• 1000Mails_70Threads_CompletesWithinBudget – the performance scenario:
  1 000 mails (70 threads × 7 + 510 standalone), 40 rotating sender
  addresses (20 with DB contacts). Times and reports two scenarios:
    - Default first-page fetch (100 mails) + expansion of one partial
      thread (expects > 100 mails returned).
    - Full load of all 1 000 mails with threading enabled (expects
      exactly 1 000 mails returned, all 70 threads intact, < 5 s).

  Elapsed times for both scenarios are written to xUnit test output so
  they appear in CI logs and can be tracked across builds.

https://claude.ai/code/session_018bqahGc6zi95JJhc2aARKS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-01 09:14:02 +01:00
Burak Kaan Köse 0e742c7a8f Resolving warnings and treating warnings as errors in WinUI project. (#824) 2026-02-27 20:12:43 +01:00
Burak Kaan Köse d2fce5eee1 Add PR GitHub Actions workflow to build WinUI and run Core tests on Windows (#823) 2026-02-27 14:16:31 +01:00
Burak Kaan Köse 5c510fd7b0 Remove single entry and mode launch on ctrl press. 2026-02-27 11:00:25 +01:00
Burak Kaan Köse e1ce85698c Fix couple issues with starting mode. 2026-02-27 10:22:52 +01:00
Burak Kaan Köse 4b22608bc5 Badges are always creatd on wino mail. 2026-02-25 02:05:05 +01:00
Burak Kaan Köse 3a39266121 Simplified compoper and rendering logic through messages. 2026-02-25 01:41:48 +01:00
Burak Kaan Köse 5d46ea73db Route mail/calendar toasts to their respective app entries (#821) 2026-02-25 01:36:26 +01:00
Burak Kaan Köse d51f4a7a23 Add SQLite indexes and enable foreign key enforcement (#820) 2026-02-24 11:08:46 +01:00
Burak Kaan Köse 79a81710f0 Improving thread mapping for all synchronizers. 2026-02-23 01:51:44 +01:00
Burak Kaan Köse c5a631da6f Grace period for local drafts. 2026-02-23 01:02:59 +01:00
Burak Kaan Köse 33672ab0aa Local draft resent and default app mode settings. 2026-02-22 17:55:57 +01:00
Burak Kaan Köse 311b3c77c8 Wino Calendar app entry. (#819)
* Double app entry.

* New icon for sys tray
2026-02-22 15:13:39 +01:00
Burak Kaan Köse 17ca32c537 Support large Outlook attachments via upload sessions when sending drafts (#814)
* Add Outlook large attachment upload sessions for send draft

* UI thread executino of draft busy state.

* Limit outlook attachment limit to max allowed per attachment.
2026-02-21 16:14:55 +01:00
Burak Kaan Köse 9d3f0bddde Add manual live ImapSynchronizer coverage tests (#818)
* Add manual live IMAP synchronizer tests

* Fixing build errors and testing.
2026-02-21 11:47:16 +01:00
Burak Kaan Köse 7f198bad92 Implement mail and calendar item synchronizer state (#815)
* Track pending sync operations per mail/calendar item

* Updated progressbar for in progress drafts
2026-02-21 10:53:39 +01:00
Burak Kaan Köse a912ada890 Fixing some messaging issues with calendar add/delete. 2026-02-20 10:03:16 +01:00
Burak Kaan Köse 317113a1b3 Fixing timezone issue with caldav. 2026-02-19 02:09:36 +01:00
Burak Kaan Köse 564cb0b16f Fix double init on calendar days. 2026-02-19 01:37:43 +01:00
Burak Kaan Köse ab0810f710 Fixing the delta sync for caldav. 2026-02-18 20:43:10 +01:00
Burak Kaan Köse 7a13ae0ac8 Add manual live CalDAV service workflow tests (#816) 2026-02-18 13:44:58 +01:00
Burak Kaan Köse c8e1678e55 Fix HtmlPreviewVisitor regressions and add sanitization tests (#813) 2026-02-17 22:12:27 +01:00
Burak Kaan Köse f49d276f5a Add dedicated Wino.Mail.ViewModels.Tests coverage for WinoMailCollection (#812)
* Add WinoMailCollection tests in dedicated ViewModels test project

* Fix WinoMailCollection tests flattening without SelectMany
2026-02-17 15:45:29 +01:00
Burak Kaan Köse 05112d6a35 Dispatch WebView2 runtime toast notification on UI thread (#811) 2026-02-16 16:32:47 +01:00
Burak Kaan Köse fec49ce6f8 Proper cleanup of account on the UI when its deleted. 2026-02-16 01:56:22 +01:00
Burak Kaan Köse 31a7faeef9 Handle operation execution errors in rendering page. 2026-02-16 01:39:53 +01:00
Burak Kaan Köse dae7d046c4 Calendar metadata fetch after creating account. 2026-02-15 19:57:48 +01:00
Burak Kaan Köse d428a6ce7a Ignore local calendar applying changes to prevent duplicate operations. 2026-02-15 19:44:07 +01:00
Burak Kaan Köse ff25db3fea Add busy state support for calendar item view models (#810) 2026-02-15 19:26:06 +01:00
Burak Kaan Köse 2baa87daeb Add IMAP local calendar operation tests using in-memory DB (#807)
* Add IMAP local calendar operation handler tests

* Fix tests.

* Fix calendar item show as not updating.

* Create one default calendar for local calendar accounts.
2026-02-15 18:40:32 +01:00
Burak Kaan Köse 42e51571a8 Bunch of calendar implementation thing. 2026-02-15 11:27:30 +01:00
Burak Kaan Köse acf0f649e8 CalDav synchronizer, new IMAP setup/edit page. 2026-02-15 02:20:18 +01:00
Burak Kaan Köse 64b9bfc392 Flag changes for uid based imap sync 2026-02-14 13:22:16 +01:00
Burak Kaan Köse 744145be06 Refactored impa synchronization. 2026-02-14 12:52:17 +01:00
Burak Kaan Köse 4a0dcd2899 Remove old project files. 2026-02-13 20:45:55 +01:00
Burak Kaan Köse 92df726f34 Batch flip-view range updates for programmatic calendar navigation (#805)
* Batch calendar range updates during programmatic navigation

* Refine programmatic calendar navigation batching state
2026-02-13 14:37:24 +01:00
Burak Kaan Köse dbd5812c45 Fix null handling in WinoCalendarView date range updates (#806) 2026-02-13 10:58:25 +01:00
Burak Kaan Köse 884f000058 Calendar stuff. 2026-02-13 03:09:13 +01:00
Burak Kaan Köse e936c431a2 Search improvements. 2026-02-12 18:57:55 +01:00
Burak Kaan Köse b01fa4e4ba Event details page improvements, calendar item update source. 2026-02-12 18:04:29 +01:00
Burak Kaan Köse 96dcdc8e03 Auto sync trigger and cancellation support. 2026-02-11 14:50:59 +01:00
Burak Kaan Köse 96d2efb3f0 Remove semantic zoom support. 2026-02-11 14:50:48 +01:00
Burak Kaan Köse 37199d84cb Fixed the caching issue that causes mails to be not removed. Improved drag/drop. 2026-02-11 11:34:50 +01:00
Burak Kaan Köse 52ee5f1d8a UI visuals for mail calendar items, calendar reminders. 2026-02-11 01:49:29 +01:00
Burak Kaan Köse 870a5e2bf6 Calendar - mail mapping. 2026-02-10 21:35:55 +01:00
Burak Kaan Köse 10dd42b63f Thread UI fixes. 2026-02-10 01:03:03 +01:00
Burak Kaan Köse 0999c71578 Contacts, thread animation and image preview control improvements. 2026-02-09 22:39:30 +01:00
Burak Kaan Köse e559a79506 Generic 404 handler for synchronizers. 2026-02-08 22:20:38 +01:00
Burak Kaan Köse 1747ed84a8 Disable logging synchronizer exceptions to sentry. 2026-02-08 19:43:13 +01:00
Burak Kaan Köse 22c6452227 Editor optimizations 2026-02-08 10:35:24 +01:00
Burak Kaan Köse ad9b94d407 Removed the INC registrations for list view items. 2026-02-08 01:41:32 +01:00
Burak Kaan Köse 9f13bcd991 Collection optimizations. 2026-02-08 01:41:09 +01:00
Burak Kaan Köse 5bfa61a218 Create sub folder, delete folder, storage settings, some ui adjustments on threads. 2026-02-07 19:47:21 +01:00
Burak Kaan Köse 2cd03d5fec Fix unrealized container unselected issue with the threads. 2026-02-07 15:50:23 +01:00
Burak Kaan Köse c7fb648387 Thread selection improvements 2026-02-07 15:29:19 +01:00
Burak Kaan Köse 331b966556 Info panel for synchronizers in shell. 2026-02-07 14:03:41 +01:00
Burak Kaan Köse d28de50ec6 Fixing outlook attachments, re-using compose page and some additional fixes on the mime headers for outlook. 2026-02-07 13:10:57 +01:00
Burak Kaan Köse 1ec8d5bbf2 Gmail drafting 2026-02-06 21:46:30 +01:00
Burak Kaan Köse 4374d19ac2 Threading improvements. 2026-02-06 20:13:44 +01:00
Burak Kaan Köse 071f1c9786 Refactored all synchronizers to deal with some of the chronic issues. 2026-02-06 01:18:12 +01:00
Burak Kaan Köse d1425ca9ca Ignore claude permissions. 2026-02-06 01:18:10 +01:00
Burak Kaan Köse 2fd600d47d Partial Busy state for mark as read requests 2026-02-05 12:48:38 +01:00
Burak Kaan Köse 0eba778158 Mail update source. 2026-01-27 21:21:04 +01:00
Burak Kaan Köse b343152f14 Some experiments. 2026-01-27 20:37:18 +01:00
Wynn Zeng 4f65502c95 fix(ui): batch UI updates for bulk mark-as-read to prevent UI freeze (#786) 2026-01-27 20:25:05 +01:00
Burak Kaan Köse 31097e42a9 Reacting calendar changes 2026-01-20 00:30:24 +01:00
Burak Kaan Köse 319b0af305 Global back listener for mouse. 2026-01-06 17:34:06 +01:00
Burak Kaan Köse f105c2f8f0 Settings page and manage accounts navigation options. 2026-01-06 17:23:58 +01:00
Burak Kaan Köse 7cc201f423 ShowAs stripe for calendar control template. 2026-01-06 12:54:47 +01:00
Burak Kaan Köse a23a99cc8d Join online for quick popup. 2026-01-06 12:07:22 +01:00
Burak Kaan Köse be6b23c47b AOT safe panels. 2026-01-06 11:45:03 +01:00
Burak Kaan Köse f8333aab10 Single isntances and some updates shit. 2026-01-06 11:11:37 +01:00
Burak Kaan Köse d279c0a8dd Fix syncing ocurrences. 2026-01-05 15:10:37 +01:00
Burak Kaan Köse bd8867dba6 Potential nre. 2026-01-05 15:10:33 +01:00
Burak Kaan Köse 3d07328f47 Calendar invitations for Mail part of the app. 2026-01-05 00:21:07 +01:00
Burak Kaan Köse 0b0f6b8d8e Show as localization. 2026-01-04 13:25:08 +01:00
Burak Kaan Köse 4603b1fb14 Calendar attachments. 2026-01-03 23:59:37 +01:00
Burak Kaan Köse c8ef031e7d Some changes. 2026-01-03 20:46:03 +01:00
Burak Kaan Köse 9877656eea RSVP options. 2026-01-03 19:33:36 +01:00
Burak Kaan Köse a64627e7d6 Reminders. 2026-01-01 15:02:40 +01:00
Burak Kaan Köse 3b485dc1fe Event details UI improvements. 2026-01-01 10:07:56 +01:00
Burak Kaan Köse e71c050724 Item update and delete scenarios. 2025-12-31 15:33:13 +01:00
Burak Kaan Köse d54a9f6279 copilot instructions 2025-12-31 14:15:12 +01:00
Burak Kaan Köse f917e4a721 New setting options. 2025-12-31 14:09:57 +01:00
Burak Kaan Köse 61fb10a951 Calendar settings on settings page. 2025-12-31 13:28:53 +01:00
Burak Kaan Köse d3704a0f09 More coverage for Esc to unselect all items. 2025-12-31 11:08:30 +01:00
Burak Kaan Köse c584929db5 Fix outlook sync. 2025-12-30 23:49:25 +01:00
Burak Kaan Köse ea4cf20746 +2 years sync for Outlook 2025-12-30 23:41:53 +01:00
Burak Kaan Köse 2056a2d783 Handle deleted events. 2025-12-30 23:32:00 +01:00
Burak Kaan Köse b81ab0ca15 Creating events. 2025-12-30 11:59:54 +01:00
Burak Kaan Köse 70ac2d2bea New grouped collection for quick event dialog. 2025-12-30 10:36:27 +01:00
Burak Kaan Köse 07f3dabff6 win2d -> skia, some improvements on rendering. 2025-12-30 10:02:24 +01:00
Burak Kaan Köse 72e43e4b7a Recalculate recurrences when a new event added. 2025-12-30 08:51:50 +01:00
Burak Kaan Köse 0519bf86b3 I dont know some improvements on reacting calendar changes. 2025-12-29 23:13:32 +01:00
Burak Kaan Köse 6ba2f1f3e2 Make sure outlook correctly calls datetime for delta api. 2025-12-29 14:46:31 +01:00
Burak Kaan Köse 8613e92b31 Fixed the display date of the calendar items. Created test project for core library, included tests for recurring calendar events. 2025-12-29 14:10:09 +01:00
Burak Kaan Köse f79305f0a6 Fix the AOT issue with custom binding of IsSelected property through CVS in Mail List. 2025-12-28 07:28:20 +01:00
Burak Kaan Köse 0f6aa66b21 Small warning. 2025-12-28 07:27:27 +01:00
Burak Kaan Köse 51540c89d1 Fix auto nav to calendar on launch. 2025-12-28 06:58:06 +01:00
Burak Kaan Köse a5227abd40 Some UI shit. 2025-12-27 19:16:24 +01:00
Burak Kaan Köse 014b5aa671 Initial integration. 2025-12-26 20:46:48 +01:00
Burak Kaan Köse 10b85ea135 Fixing attachment icons. 2025-12-25 19:48:42 +01:00
Burak Kaan Köse f6e94e89c9 Fixing an issue where DeleteAsync calls expect PK. 2025-12-25 17:21:23 +01:00
Burak Kaan Köse 8a68fafedf Fixed an issue with loading mails with infinite scroll. 2025-12-15 21:06:13 +01:00
Burak Kaan Köse 7f8c6776fc New Crowdin updates (#783)
* New translations resources.json (Romanian)

* New translations resources.json (Spanish)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (Italian)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Indonesian)
2025-12-15 21:05:53 +01:00
dayblox 6e5efa69c9 Fix typo in documentation comment (#782) 2025-12-15 21:05:17 +01:00
Burak Kaan Köse 9fbbd00dc5 Object deleted error fix. 2025-11-30 17:51:44 +01:00
Burak Kaan Köse a8a5d3c3d6 Fixed the issue with mail rendering page not getting disposed properly. 2025-11-24 20:54:57 +01:00
Maicol Battistini beb3bf9d1d feat: S/MIME signing and encryption (#693)
* feat: add S/MIME certificate management

- Introduced `ISmimeCertificateService` interface for managing S/MIME certificates.
- Implemented `SmimeCertificateService` class to handle certificate operations.
- Updated `WinoPage` enum to include `SignatureAndEncryptionPage`.
- Added resource entries in `resources.json` for S/MIME related messages.
- Created `SignatureAndEncryptionPage` view and logic for user interaction.
- Modified configuration files to integrate the new service and page.
- Updated project files to include necessary dependencies for certificate management.

* refactor(SmimeCertificateService): ♻️ Use constant for certificate name

Refactored the `SmimeCertificateService` to replace the hardcoded string "Wino Mail Certificate" with a constant `CertificateFriendlyName`. This change enhances code maintainability by centralizing the definition of the certificate's friendly name.

• Introduced a constant for the certificate's friendly name.
• Updated the certificate retrieval and import logic to use the new constant.

* feat(alias):  Add S/Mime certificate selection for every alias

Added new properties and methods in `MailAccountAlias` to manage signing and encryption certificates, including their thumbprints. This enhancement allows for better handling of S/Mime certificates within the application.

• Introduced new properties for signing and encryption certificates.
• Updated `resources.json` with new translations for S/Mime certificates.
• Enhanced `AliasManagementPageViewModel` to include a dependency on the S/Mime certificate service and updated alias loading methods.
• Modified `AliasManagementPage.xaml` to include ComboBox controls for selecting certificates.
• Implemented methods in `AliasManagementPage.xaml.cs` to handle certificate selection from dropdowns.

This change improves the user experience by allowing users to select and manage their S/Mime certificates directly within the alias management interface.

* feat(mail):  Add S/MIME support and file picker updates

Enhanced the `MailRenderModel` class by adding a new property `IsSmimeSigned` to indicate if an email is S/MIME signed. The constructor has been updated to accept `MailRenderingOptions`.

Updated the file selection logic in `DialogServiceBase` to replace the `FolderPicker` with a `FileSavePicker`, streamlining the process of saving files. Removed unnecessary commented code and added logic to handle file extensions.

In `MailRenderingPageViewModel`, a new property `IsSmimeSigned` reflects the S/MIME status of the current render model, along with a new method `ShowSmimeCertificateInfoAsync` to display S/MIME certificate details.

Added a `HyperlinkButton` in `MailRenderingPage.xaml` to indicate S/MIME status, which is only visible for signed emails, providing a tooltip and command for more information.

In `MimeFileService`, implemented logic to detect S/MIME signatures in messages and exclude S/MIME signature parts from attachments.

* refactor(viewmodel): ♻️ Replace dialog service messages

Refactored the `SignatureAndEncryptionPageViewModel.cs` to replace calls to `_dialogService.ShowMessageAsync` with `_dialogService.InfoBarMessage`. This change improves the handling of success messages during certificate import and removal processes.

* feat(mail):  Add S/MIME encryption indicator

Implemented support for S/MIME email handling in the MailRenderingPageViewModel. This includes the addition of a new property to check if an email is encrypted and updates to methods for displaying S/MIME certificate information.

A new column was added in the MailRenderingPage.xaml to indicate if an email is encrypted, along with updated tooltips and commands. The MimeFileService was also modified to detect S/MIME encryption and to exclude S/MIME signature certificates during attachment processing.

* fix: Added missing property

* feat: Added S/Mime decryption and signing verification and improvements

* i18n(resources): 🌐 Add S/MIME translation strings

Added new translation strings for S/MIME functionalities in `resources.json`, including messages for signatures and certificates in both English and Italian. The code has been updated to utilize these new translation strings, enhancing the application's internationalization.

Updated `MailRenderingPageViewModel.cs` to use the new translation strings for signature and certificate messages, improving code readability and consistency with translations. Additionally, the tooltips for S/MIME signing and encryption buttons in `MailRenderingPage.xaml` have been updated to use the new translation strings, enhancing the user experience for Italian-speaking users.

* fix: Extract body from MultipartSigned message

* feat(smime):  Enhance S/MIME certificate handling

Updated the `SmimeCertificateService` to improve the loading of PKCS12 certificate collections by adding `X509KeyStorageFlags.DefaultKeySet` and `X509KeyStorageFlags.Exportable` for better key management.

In `ComposePageViewModel`, imported necessary namespaces for S/MIME certificate handling and added a new dependency for `ISmimeCertificateService`. Implemented logic in `OpenAttachmentAsync` to load alias certificates and manage message signing and encryption based on user-selected certificates.

This change enhances the security and flexibility of email handling within the application.

* feat: Replaced Smime encryption certificate combobox with checkbox

Cert selection is useless for encryption

* feat: Added S/Mime togglebuttons when composing an email

* i18n(translations): 🌐 Add new composer translations

Added new translation strings for composer features, including themes, text formatting, and S/MIME signing and encryption options. Updated button labels to utilize these new strings, enhancing the application's internationalization.

Additionally, removed an obsolete string related to S/MIME certificate file information.

* Example for relay command and fix settings pages runtime error

* refactor(viewmodel): ♻️ Update certificate import/export commands

Refactored the certificate import and export commands in the `SignatureAndEncryptionPageViewModel`. Changed methods from `async void` to `async Task` for better error handling and tracking of asynchronous operations. Added `[RelayCommand]` attributes to improve adherence to the MVVM pattern.

Updated the XAML file to bind buttons directly to the new command methods, removing the need for event handlers. This enhances separation of concerns and simplifies the code.

Removed obsolete event handlers from the code-behind file, streamlining the implementation.

* fix: export folderPath parameter contains file name

* fix: QRESYNC initial modseq should be 1 (#734)

* Fix typo in reorder accounts dialog (#754)

* fix: Missing commas in translations files

* fix: merge issues

* Fix mege conflicts.

* Some more conflict fixes.

* Fixing context.

* Fixing saving file with suggested file name.

---------

Co-authored-by: Aleh Khantsevich <aleh.khantsevich@gmail.com>
Co-authored-by: Konstantin Shkel <null+github@pcho.la>
Co-authored-by: Cas Cornelissen <cas.cornelissen@onefinity.io>
Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-11-23 20:56:57 +01:00
Burak Kaan Köse 1a2590e2c3 Missing import. 2025-11-23 17:05:11 +01:00
Burak Kaan Köse 8858ef08c2 Better progress for outlook synchronizer. 2025-11-23 17:04:38 +01:00
Burak Kaan Köse 4520e16048 Make sure gmail sdk is trimmable. 2025-11-23 16:48:54 +01:00
Burak Kaan Köse 56cd29429e Remove Bindings for maill ist page. 2025-11-16 01:56:10 +01:00
Burak Kaan Köse 07aeaf8c8f Removal of Bindings 2025-11-16 00:23:23 +01:00
Burak Kaan Köse a2c7e5f29a Remove old projects. 2025-11-16 00:08:45 +01:00
Burak Kaan Köse b3130d9441 New assets. 2025-11-16 00:02:55 +01:00
Burak Kaan Köse 0dd907e314 Merge core project into winui project. 2025-11-15 14:52:01 +01:00
Burak Kaan Köse 12a39064dc Some item templates and removal of sqlkata. 2025-11-15 13:29:02 +01:00
Burak Kaan Köse b356af8eb4 Main app aot compatibility. 2025-11-14 18:51:48 +01:00
Burak Kaan Köse ae64094feb Make winui core library aot compatible. 2025-11-14 14:48:03 +01:00
Burak Kaan Köse 472cc3d7f2 Fix warnings for core view models. 2025-11-14 14:44:56 +01:00
Burak Kaan Köse dbaed6094b Make core library aot compatible. 2025-11-14 14:42:05 +01:00
Burak Kaan Köse 8cb8f27e00 Make services aot compatible. 2025-11-14 14:28:10 +01:00
Burak Kaan Köse d9ef81729f Enable aot for libs 2025-11-14 13:59:38 +01:00
Burak Kaan Köse d592d1c235 Nuget bump 2025-11-14 13:51:46 +01:00
Burak Kaan Köse e185301277 Fix missing window handler for outlook authenticator. 2025-11-14 13:29:49 +01:00
Burak Kaan Köse 249a950dc1 Fix soem dispaly date issues. 2025-11-14 12:56:37 +01:00
Burak Kaan Köse 540a4e5117 Fix single instancing. 2025-11-14 12:51:19 +01:00
Burak Kaan Köse 3d5da92c74 Revert debug code. 2025-11-14 12:51:12 +01:00
Burak Kaan Köse 88fe141b16 Handle attention in sync manager. 2025-11-14 12:31:24 +01:00
Burak Kaan Köse 87d2ffdb71 Remove test code 2025-11-14 12:31:13 +01:00
Burak Kaan Köse 13cb3a1042 Account attentions. 2025-11-14 12:12:13 +01:00
Burak Kaan Köse 6be271565e Toast actions. 2025-11-14 11:37:26 +01:00
Burak Kaan Köse 8482171bf2 Fixed the issue with single item context menus. 2025-11-12 18:52:15 +01:00
Burak Kaan Köse c277893145 Fixed selected style for single mail ittem list view item. 2025-11-12 18:52:03 +01:00
Burak Kaan Köse 9a0290d7a6 Handling actions on toast notifications when the app is running. 2025-11-12 18:51:53 +01:00
Burak Kaan Köse 777219ab87 Toast notification navigations and some improvements for list view selection. 2025-11-12 15:44:43 +01:00
Burak Kaan Köse 16e06af76f Fix unknown sender issue. 2025-11-12 00:39:37 +01:00
Burak Kaan Köse 3b776ec1bd Fixing selected item effect. 2025-11-10 01:47:05 +01:00
Burak Kaan Köse 175ed24a66 Some selections. 2025-11-09 21:36:07 +01:00
Burak Kaan Köse 5f9b51e4db Some threading stuff. 2025-11-01 21:46:23 +01:00
Burak Kaan Köse ae9e35e091 Fix the sorting when adding mails. 2025-11-01 12:35:47 +01:00
Burak Kaan Köse b60832a270 Get rid of the mail item queue system. Go back to 6 months initial sync strategy. 2025-11-01 12:11:05 +01:00
Burak Kaan Köse 5186b14905 Initialize the web editor. 2025-11-01 12:10:44 +01:00
Burak Kaan Köse 2a67a1e961 draft header 2025-11-01 01:04:04 +01:00
Burak Kaan Köse 4d0d2ff099 Graph rate limit handler. 2025-10-31 19:53:48 +01:00
Burak Kaan Köse 37b8a382a8 System icon. 2025-10-31 19:53:43 +01:00
Burak Kaan Köse f06273aa77 Thread safe collections. 2025-10-31 19:53:31 +01:00
Burak Kaan Köse 600d1b7d38 Retry downloading in batches for Outlook 2025-10-31 12:13:54 +01:00
Burak Kaan Köse 9e74fa9578 Fix the issue where sent and draft items are added to the existing folder regardless. 2025-10-31 11:26:51 +01:00
Burak Kaan Köse 282655dca8 Fix crash 2025-10-31 01:47:33 +01:00
Burak Kaan Köse 3cc1d10b87 some changes for progress 2025-10-31 01:41:51 +01:00
Burak Kaan Köse 4bf8f8b3d3 Bunch of improvements i dunno. 2025-10-31 00:51:27 +01:00
Burak Kaan Köse 2d81d07c0a Mail queues. 2025-10-30 17:15:05 +01:00
Burak Kaan Köse b0ac6e4e55 Demo contacts page. 2025-10-29 19:35:04 +01:00
Burak Kaan Köse 3db1fd0dde Cleanup main list view on page navigation. 2025-10-29 18:45:14 +01:00
Burak Kaan Köse df0eae256c Add copilot instructions. 2025-10-29 18:44:58 +01:00
Burak Kaan Köse 9c348f79d7 Add drag start in list view. 2025-10-29 18:44:49 +01:00
Burak Kaan Köse 525950a4da Fix for sender name and adress not updating in threads. 2025-10-29 18:44:38 +01:00
Burak Kaan Köse 394af3ba0a Gmail synchronizer improvements. 2025-10-29 18:44:15 +01:00
Burak Kaan Köse 27177acff7 Load more command for list view. 2025-10-29 17:29:42 +01:00
Burak Kaan Köse 864d68b6ac Publc partial 2025-10-29 17:03:17 +01:00
Burak Kaan Köse c2e6c68f50 Fixing modiufiers. 2025-10-29 17:02:58 +01:00
Burak Kaan Köse b44fb5c45a Keyboard shortcuts dialog. 2025-10-29 16:26:46 +01:00
Burak Kaan Köse abaab18eb7 Auto mark as read fix and del keyboard accelerator. 2025-10-28 16:47:06 +01:00
Burak Kaan Köse d02205fba3 Item vm prop changes. 2025-10-28 14:43:22 +01:00
Burak Kaan Köse c461a4daed Swipe action implementations. 2025-10-27 23:22:55 +01:00
Burak Kaan Köse 4f85fa6ba9 New list view items. 2025-10-27 22:52:26 +01:00
Burak Kaan Köse 4eea21c4f5 Better prop change cleanup. 2025-10-27 12:53:15 +01:00
Burak Kaan Köse 7816400c01 Remove collapsing animation on expander. 2025-10-27 12:52:56 +01:00
Burak Kaan Köse 103841c364 More interactions with threads. 2025-10-27 01:43:36 +01:00
Burak Kaan Köse 54ac07f6fb Container cleanups. 2025-10-27 01:00:38 +01:00
Burak Kaan Köse d9fc365aeb Intercepting containers for threads. 2025-10-26 23:35:09 +01:00
Burak Kaan Köse 79d5b6ed40 New WinoListView implementation with multiple selections. 2025-10-26 14:53:22 +01:00
Burak Kaan Köse d4c8ae6cb7 Attempt to bring back ListView. 2025-10-25 10:54:38 +02:00
Burak Kaan Köse 6c37c9e786 Misc fixes. 2025-10-25 10:22:35 +02:00
Burak Kaan Köse ff1c3dece3 Some items view improvements for keyboards accelerators. 2025-10-22 03:45:38 +02:00
Burak Kaan Köse 449c1d3f4d Fixing some issues with ItemsView and selections. 2025-10-21 22:08:56 +02:00
Burak Kaan Köse ae7d576967 Fixing system tray icon. 2025-10-21 15:40:19 +02:00
Burak Kaan Köse 3b3c878d0e Fix resetting selected item on loading more. 2025-10-21 01:57:08 +02:00
Burak Kaan Köse 057edb5488 Custom print dialog and better message registrations 2025-10-21 01:27:29 +02:00
Burak Kaan Köse 4191b7314f Custom print dialog. 2025-10-20 21:10:29 +02:00
Burak Kaan Köse baf4141773 PrintUI thing. 2025-10-20 21:10:14 +02:00
Burak Kaan Köse 7a7281f2fa Remove codepages since it'll be pruned. 2025-10-20 19:18:42 +02:00
Burak Kaan Köse 8e16908fce Fix flickering on item selection and context menus. 2025-10-20 19:17:52 +02:00
Burak Kaan Köse 5e0a0456c4 Some dispatcher fixes. 2025-10-20 18:27:09 +02:00
Burak Kaan Köse fb56001a52 Minimum download logic. 2025-10-20 18:27:02 +02:00
Burak Kaan Köse ecff97419b Default theme is back. Container selection functionality etc. 2025-10-18 22:16:28 +02:00
Burak Kaan Köse ad135c5e32 Bunch of changes for ItemsView and threads. 2025-10-18 11:45:10 +02:00
Cas Cornelissen 89f4b4c05d Fix typo in reorder accounts dialog (#754) 2025-10-17 20:35:53 +02:00
Konstantin Shkel 70a1f1325f fix: QRESYNC initial modseq should be 1 (#734) 2025-10-17 20:35:26 +02:00
Burak Kaan Köse 522a2da114 ItemsView thing. 2025-10-12 16:25:15 +02:00
Burak Kaan Köse 7ca6a65559 Outlook sync improvements. 2025-10-12 16:23:33 +02:00
Burak Kaan Köse 309e891594 Outlook auth fix and actually syncing. 2025-10-06 17:46:00 +02:00
Burak Kaan Köse 9623c2e6d2 Synchronization manager. 2025-10-04 23:10:07 +02:00
Burak Kaan Köse 3b1eff1702 Tray icon implementation. 2025-10-04 15:46:05 +02:00
Burak Kaan Köse a00ff3df46 Fix settings navigation and fix flicker on personalizaton nav. 2025-10-04 14:44:51 +02:00
Burak Kaan Köse 2f5d4dad9a Shell title bar buttons adjustments. 2025-10-04 13:40:35 +02:00
Burak Kaan Köse 20ee4c3567 title bar shell content 2025-10-03 22:12:27 +02:00
Burak Kaan Köse accffe8ef6 Remove connection manager. 2025-10-03 21:55:23 +02:00
Burak Kaan Köse e42ebb49ae Remove old theme service completely. 2025-10-03 21:17:41 +02:00
Burak Kaan Köse 1c49b69332 Couple aot fixes. 2025-10-03 21:13:26 +02:00
Burak Kaan Köse 229006c51d New theme service that supports window backdrop. 2025-10-03 21:04:23 +02:00
Burak Kaan Köse 15b6f5f6fb Some border adjustments 2025-10-03 15:59:37 +02:00
Burak Kaan Köse ec7ac44b87 fix nre on updating thumbnails 2025-10-03 15:49:44 +02:00
Burak Kaan Köse 7b41f558d4 Stub 2025-10-03 15:46:38 +02:00
Burak Kaan Köse 2bec513d2c Some shell/ themes improvements 2025-09-29 19:09:48 +02:00
Burak Kaan Köse f6bf080c9e Remove unused projects. 2025-09-29 11:24:13 +02:00
Burak Kaan Köse 734a3d75db Replace Core.UWP namespace with Core.WinUI 2025-09-29 11:23:44 +02:00
Burak Kaan Köse e67b893ae4 Initial WinUI switch. 2025-09-29 11:16:14 +02:00
Burak Kaan Köse f9c53ca2c9 New Crowdin updates (#724)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Bulgarian)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Lithuanian)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Slovak)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)
2025-07-30 23:43:07 +02:00
Burak Kaan Köse 21f9c7cf6d Deprecation of Application Insights for Sentry.IO (#723)
* Remove Application Insights implementation and implement new Sentry.IO SDK

* Remove test exception.
2025-07-30 23:36:10 +02:00
Maicol Battistini 43283b7218 feat(notification): Remove notification when read externally (#707)
* feat(notification):  Add notification removal feature

Implemented a new method `RemoveNotificationAsync` in the `INotificationBuilder` interface to allow the removal of toast notifications for specific emails identified by a unique ID.

This change enhances the notification management by ensuring that notifications can be cleared when emails are marked as read. The `NotificationBuilder` class has been updated to include logic for removing existing notifications and to use the unique ID as a tag for the toast notifications, facilitating their removal. Additionally, the `AppShellViewModel` has been modified to call this new method when an email is updated and marked as read.

This improvement aims to provide a better user experience by keeping the notification area relevant and up-to-date.

* feat(notification):  Add MailReadStatusChanged event handling

Introduced a new event system for handling email read status changes. This includes the addition of a listener in `NotificationBuilder` that removes notifications when an email is marked as read.

• Added `MailReadStatusChanged` record to represent the event.
• Registered a listener in `NotificationBuilder` to handle notification removal.
• Removed the `OnMailUpdated` method from `AppShellViewModel`, delegating notification management to the new event system.
• Updated `MailService` to send `MailReadStatusChanged` events when emails are marked as read.

This change improves the communication between components and enhances the notification management system.

* refactor: Remove comments

* Little cleanup.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-07-26 12:51:53 +02:00
Maicol Battistini c2bb07ff3d feat(preferences): Add email sync interval setting (#710)
* feat(preferences):  Add email sync interval setting

Introduced a new property `EmailSyncIntervalMinutes` in the `IPreferencesService` interface to allow users to configure the email synchronization interval in minutes. This feature enhances user control over email sync behavior.

• Updated `resources.json` to include translations for the new setting.
• Implemented the logic for the new property in `PreferencesService.cs`, with a default value of 3 minutes.
• Added binding and UI support in `AppPreferencesPageViewModel.cs` and `AppPreferencesPage.xaml` to allow users to modify the sync interval.
• Integrated the new setting into `ServerContext.cs` to dynamically adjust the synchronization timer based on user preferences.

This change improves the user experience by providing customizable email synchronization settings.

* Minimum interval and added an icon.

* Proper SetProperty usage.

* Making sure the minimum sync interval is 1 in the ServerContext.

* Making sure the minimum is applied to first trigger of the sync timer.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-07-24 09:45:35 +02:00
Aleh Khantsevich 8cd7f68c30 fix save imap settings and progress ring. (#704)
Added notification that settings saved.
2025-07-07 19:28:56 +02:00
Aleh Khantsevich 3e889d8c08 Make height of single account navigation item smaller (#702)
* Make height of navigation item 50

* fix subtle and heights

* move spacing and margins

* make 52

* fix wrong heights
2025-07-02 23:41:41 +02:00
Aleh Khantsevich a01395aed3 fix tab navigation for compose page (#695) 2025-06-21 13:35:42 +02:00
Aleh Khantsevich 7b3459abff Text input should update property on each changem instead of lost focus (#694) 2025-06-21 01:45:21 +02:00
Aleh Khantsevich 9a88f798fc fix animations (#689) 2025-06-21 01:40:45 +02:00
Maicol Battistini 256fd1cce2 feat: Enhanced sender avatars with gravatar and favicons integration (#685)
* feat: Enhanced sender avatars with gravatar and favicons integration

* chore: Remove unused known companies thumbnails

* feat(thumbnail): add IThumbnailService and refactor usage

- Introduced a new interface `IThumbnailService` for handling thumbnail-related functionalities.
- Registered `IThumbnailService` with its implementation `ThumbnailService` in the service container.
- Updated `NotificationBuilder` to use an instance of `IThumbnailService` instead of static methods.
- Refactored `ThumbnailService` from a static class to a regular class with instance methods and variables.
- Modified `ImagePreviewControl` to utilize the new `IThumbnailService` instance.
- Completed integration of `IThumbnailService` in the application by registering it in `App.xaml.cs`.

* style: Show favicons as squares

- Changed `hintCrop` in `NotificationBuilder` to `None` for app logo display.
- Added `FaviconSquircle`, `FaviconImage`, and `isFavicon` to `ImagePreviewControl` for favicon handling.
- Updated `UpdateInformation` method to manage favicon visibility.
- Introduced `GetBitmapImageAsync` for converting Base64 to Bitmap images.
- Enhanced XAML to include `FaviconSquircle` for improved UI appearance.

* refactor thumbnail service

* Removed old code and added clear method

* added prefetch function

* Change key from host to email

* Remove redundant code

* Test event

* Fixed an issue with the thumbnail updated event.

* Fix cutted favicons

* exclude some domain from favicons

* add yandex.ru

* fix buttons in settings

* remove prefetch method

* Added thumbnails propagation to mailRenderingPage

* Revert MailItemViewModel to object

* Remove redundant code

* spaces

* await load parameter added

* fix spaces

* fix case sensativity for mail list thumbnails

* change duckdns to google

* Some cleanup.

---------

Co-authored-by: Aleh Khantsevich <aleh.khantsevich@gmail.com>
Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-06-21 01:40:25 +02:00
Burak Kaan Köse a8cb332232 Type fix. 2025-06-20 14:34:37 +02:00
Victor 89ea2b23a2 Replaced "Dismiss" button in notification popup with "Archive" button (#664)
* replaced "Dismiss" button in notification popup with "Archive" button

fixes https://github.com/bkaankose/Wino-Mail/issues/40

* Fixed incorrect build action for the archive icon.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-06-15 15:27:39 +02:00
Aleh Khantsevich 9b214a66c8 Added new option to hide action labels in mail rendering page (#683)
* Added option to disable labels for mail actions

* Updated spacings and section title styles in settings

* Added translations
2025-06-15 15:17:57 +02:00
Aleh Khantsevich 4c4689ec8d Flyout styles and settings animations (#682)
* Refactor and enhance settings pages and solution structure

- Added transition effects to multiple pages for enhanced UI animations.
- Moved `AboutPage` and `PersonalizationPage` to settings folder.
- Put version into settings card instead of text.

* Fixed main logo in about page and changed version styles

* revert platforms

* Remove useless imprt

* Apply this animation globally

* Added resize transition for mail rendering page

* remove entrance transition from rendering page
2025-06-15 14:54:03 +02:00
Burak Kaan Köse c4e561dee6 dotnet format refactorings. 2025-05-18 14:06:25 +02:00
Burak Kaan Köse 69bfe5b750 Fix calendar server startup. 2025-05-03 20:21:06 +02:00
Burak Kaan Köse 137b3dc2ea Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-05-03 19:08:36 +02:00
Burak Kaan Köse ea5f879181 Fixed calendar slnx build. 2025-05-03 19:08:29 +02:00
Burak Kaan Köse 25d5f34f68 Version bump 2025-05-03 19:08:22 +02:00
Dinuru Seniya c8a6df77ac Outlook Auth Fix (#653)
Issue: Account selector dialog pops up endlessly for Outlook/Live accounts. (Stored account not being correctly identified)

Fix: Ignore case differences, add null safety and remove whitespaces when retrieving stored accounts.
2025-05-02 12:12:45 +02:00
Burak Kaan Köse 7b6ac46b6a More informational message for different UPN and address for Outlook authenticator. 2025-04-26 12:25:34 +02:00
Burak Kaan Köse d77c648d54 New Crowdin updates (#646)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Bulgarian)

* New translations resources.json (Czech)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Italian)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Slovak)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Portuguese, Brazilian)
2025-04-26 11:04:03 +02:00
Burak Kaan Köse c3f47c5fa1 Check account notification preferences after the synchronization. (#647) 2025-04-26 11:02:41 +02:00
Burak Kaan Köse f37a51b46f Remove test code. 2025-04-26 10:51:14 +02:00
Burak Kaan Köse 9feb3f35c3 Synchronizer error factory implementation (#645)
* Added sync error factories for outlook and gmail.

* Implement ObjectCannotBeDeletedHandler for OutlookSynchronizer.

* Remove debug code.

* Implement del key to delete on mail list.

* Revert debug code.
2025-04-26 10:49:55 +02:00
Burak Kaan Köse 5b44cf03ce Don't report when printing is canceled. 2025-04-21 10:31:23 +02:00
Burak Kaan Köse 86a6382463 Max 1500 mails to download per-folder on initial sync for Gmail. 2025-04-21 10:15:42 +02:00
Burak Kaan Köse df991a3829 Bump nugets. 2025-04-21 10:15:05 +02:00
Grigory f243c86b50 build(nuget.config): correct nuget packageSources key name (#623) 2025-04-06 11:33:30 +02:00
Grigory b77be0a5e9 build(Wino.Server.csproj): specify RuntimeIdentifiers (#621) 2025-04-06 11:33:08 +02:00
Burak Kaan Köse 83be587c1a Make sure there are no duplicate items for providers except Gmail when creating mails. 2025-04-04 23:55:50 +02:00
Burak Kaan Köse c6048aea80 Make sure the requests are reflected to UI during synchronization. 2025-03-19 23:37:50 +01:00
Burak Kaan Köse 13b495b0f6 Fixed the Gmail sync identifier update issue and removed the batch message download. 2025-03-19 23:22:57 +01:00
Burak Kaan Köse ac64c35efa Fix for another sequence contains error. 2025-03-19 22:15:28 +01:00
Burak Kaan Köse 127b58601f Remove missing isuread property. 2025-03-18 00:12:31 +01:00
Burak Kaan Köse 1f795b45e9 More visible unread items. 2025-03-18 00:10:45 +01:00
Burak Kaan Köse d26e35ee9a Ctrl + A to select all mails. 2025-03-15 17:43:57 +01:00
Burak Kaan Köse 70e69e9dac Wino Calendar slnx 2025-03-15 15:23:26 +01:00
Burak Kaan Köse 3d88f4212d Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-03-15 15:22:43 +01:00
Burak Kaan Köse ad90a9c8f3 Fix: Sequence contains no elements while downloading Gmail messages. 2025-03-15 15:22:01 +01:00
Aleh Khantsevich b43176764b Trim all whitespaces, including \t for unsubscribe links (#599) 2025-03-06 22:34:05 +01:00
Burak Kaan Köse 77f24282e0 Fix incorrect visibility. 2025-03-01 19:43:32 +01:00
Burak Kaan Köse 533f1f1102 1.10.2 release notes. 2025-03-01 19:43:21 +01:00
Burak Kaan Köse 92c5d8bd44 New translations resources.json (Turkish) (#595) 2025-03-01 17:09:54 +01:00
Burak Kaan Köse d754ecb486 New Crowdin updates (#594)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Lithuanian)
2025-03-01 17:05:04 +01:00
Burak Kaan Köse b18987a95c Added ability to edit imap server configuration. (#593) 2025-03-01 16:53:05 +01:00
EzraWard 0daec61f31 Display app name on Win10 start tiles (#591) 2025-03-01 01:17:42 +01:00
Burak Kaan Köse 8ecf301eb8 Account colors + edit account details. (#592)
* Remove account rename dialog. Implement edit account details page.

* Remove unused folder definition.

* Adressing theming issues and adding reset button. Changing the UI a bit.

* Enable auto indent in initializer. Use service from the application.

* Adding color picker to acc setup dialog. Changing UI of edit acc details page.
2025-03-01 01:17:04 +01:00
Burak Kaan Köse 6080646e89 Don't crash on contact inserts. 2025-02-28 18:21:31 +01:00
Burak Kaan Köse 970a521b66 Pre-warmup on imap synchronizer interface. 2025-02-26 23:13:17 +01:00
Burak Kaan Köse 9b5a92f942 Changing delete logic. 2025-02-26 23:13:05 +01:00
Burak Kaan Köse c4e0f13d67 Pre warmup trigger on synchronizer creation for imaps. 2025-02-26 23:12:01 +01:00
Burak Kaan Köse b6821746d0 Locked busy scope to handle disconnections properly. 2025-02-26 23:11:49 +01:00
Burak Kaan Köse b98fc91a99 Refactoring ImapClientPool. Implemented no-op timer and pre-warmup clients logic. Disabled protocol log per-account. 2025-02-26 23:11:16 +01:00
Burak Kaan Köse bd7f7b867e Making sure missing draft folder is handling during draft creation. 2025-02-26 23:10:30 +01:00
Burak Kaan Köse 32a3fea8d7 Automatically append sent messages to sent folder for iCloud and Yahoo. 2025-02-26 22:57:08 +01:00
Burak Kaan Köse 3561beab1d Revert bump graph. 2025-02-26 22:18:25 +01:00
Burak Kaan Köse 1d1fd52cae Refactoring mail collection class. 2025-02-26 19:59:20 +01:00
Burak Kaan Köse c4ba438150 Handling of generalException and some refactorings on batch executions. 2025-02-26 19:59:11 +01:00
Burak Kaan Köse 37f0ee08b1 Bump graph API. 2025-02-26 19:22:43 +01:00
Burak Kaan Köse 240b02c94e Fix gmail mail service not enabled error. 2025-02-26 19:04:38 +01:00
Burak Kaan Köse e8142ff3df Download messages in ascending order. 2025-02-26 11:45:23 +01:00
Aleh Khantsevich 832b363da7 Improved outlook online search even more and removed redundant methods from ChangeProcessor (#586) 2025-02-24 18:53:11 +01:00
Dinuru Seniya cf8f1ecd67 Code cleanup (#585)
1.  Moved the IsBackground property assignment into the object initializer for the Thread object.

2. Replaced e.Args[e.Args.Length - 1] with e.Args[^1]

3. Added a conditional check to see if GetWindowThreadProcessId returns 0, which indicates failure. If it fails, throw a Win32Exception with the last Win32 error.

4. Removed unused assignment to the variable process

5. Changed the return type of the ConfigureServices method from IServiceProvider to ServiceProvider. It is more specific and faster.

6. Changed notifyIcon to _notifyIcon according to private var naming scheme.

7. Added the CharSet = CharSet.Unicode attribute to the DllImport declarations to specify that the string arguments should be marshaled as Unicode.
2025-02-24 09:50:44 +01:00
Burak Kaan Köse ee5129830c Gmail crash fix. 2025-02-24 09:48:07 +01:00
Aleh Khantsevich 9facfaffa8 Improved online search performance when doing local operations (#584)
* Improved online search performance when doing local operations

* Retruning an empty list on no item searches.

* Fixed an issue with batch imap downloads.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-02-23 22:17:40 +01:00
Burak Kaan Köse 31b859ba1a Release notes for v1.10.2 2025-02-23 20:58:33 +01:00
Burak Kaan Köse b0f5a24c30 New Crowdin updates (#583)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Lithuanian)
2025-02-23 19:09:27 +01:00
Burak Kaan Köse b60b594e44 Id -> ID in ENG translations. 2025-02-23 19:08:01 +01:00
Burak Kaan Köse a8cee1016b Enable default accounts synchronization and timer sync for debug builds but not if it is attached. 2025-02-23 17:24:59 +01:00
Burak Kaan Köse b551af01fa Missing archive id check for gmail synchronizer. 2025-02-23 17:16:53 +01:00
Burak Kaan Köse b178869a8e Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-02-23 17:05:53 +01:00
Burak Kaan Köse 8e1c60d5f0 Gmail - Archive/Unarchive (#582)
* Disable timer back sync for debug builds.

* Archive / unarchive feature for Gmail.

* Archive folder name override for Gmail.

* Possible crash fix when the next item is being selected after a mail is removed.

* Restore proper account selection after pin/unpin of folder.

* Making sure that incorrect arcive folder id is not saved in Gmailsynchronizer due to migration.
2025-02-23 17:05:46 +01:00
Burak Kaan Köse 71ea49439e Disable timer back sync for debug builds. 2025-02-23 16:01:51 +01:00
Burak Kaan Köse 9d0a2f6535 Ignore folder filter if label specific query is passed to Gmail. 2025-02-23 10:21:58 +01:00
Burak Kaan Köse c091fffe90 Hnadling of folder delta token 410 GONE for Outlook. 2025-02-23 00:35:13 +01:00
Burak Kaan Köse 7e05d05f94 Implemented cache reset for Gmail history id expiration. (#581) 2025-02-22 23:09:53 +01:00
Burak Kaan Köse bd5b51c62f Added capability to detect disabled gmail service for Google Workspace accounts during account creation. (#580) 2025-02-22 17:51:38 +01:00
Burak Kaan Köse 1d5eb2eced Added simple validations for advanced imap setup dialog to prevent users from making mistakes. (#579) 2025-02-22 01:54:52 +01:00
Aleh Khantsevich 5073ead8fe Extract webvieweditor to share between compose page and signature editor (#578)
* initial work for webview editor control

* moved more stuff to editor itself

* revert packages.props indention changes

* move alignment logic

* Migrate signature editor to new control

* move background to editor control

* Some polishing

* Fixed the corner glitch issue with dark theme.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-02-22 00:43:39 +01:00
Burak Kaan Köse f61bcb621b Online Search (#576)
* Very basic online search for gmail.

* Server side of handling offline search and listing part in listing page.

* Default search mode implementation and search UI improvements.

* Online search for Outlook.

* Very basic online search for gmail.

* Server side of handling offline search and listing part in listing page.

* Default search mode implementation and search UI improvements.

* Online search for Outlook.

* Online search for imap without downloading the messages yet. TODO

* Completing imap search.
2025-02-22 00:22:00 +01:00
Burak Kaan Köse 42b695854b Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-02-20 00:54:46 +01:00
Burak Kaan Köse 496ae8b1b2 Download imap messages in ascending order. 2025-02-20 00:54:41 +01:00
Aleh Khantsevich 4215a2592f Remove last simicolon in to/cc/bcc (#574) 2025-02-18 20:51:02 +01:00
Sean Chen bca62033a1 Log unexpected exceptions on sync failure (#569) 2025-02-16 21:15:31 +01:00
Burak Kaan Köse 18a91f9223 Fix condstore synchronization. 2025-02-16 20:40:53 +01:00
Burak Kaan Köse 474d7c7a26 New Crowdin updates (#568)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Lithuanian)
2025-02-16 18:13:32 +01:00
Burak Kaan Köse 3f9a51ff46 Fix portuguese - brazil typo. 2025-02-16 17:06:07 +01:00
Burak Kaan Köse df3b5c41f9 Clicking on loaded account menu item will automatically go to Inbox. 2025-02-16 16:56:59 +01:00
Burak Kaan Köse 8800d11ab0 Lower the amount of text needed to start auto-complete in composer page to 2. 2025-02-16 16:56:42 +01:00
Burak Kaan Köse f021834ceb Fixing diagnostic id being not saved properly. 2025-02-16 16:42:48 +01:00
Burak Kaan Köse f54a39a549 Fix missing ; for 'you' 2025-02-16 16:33:02 +01:00
Burak Kaan Köse c312ff3faf Ignore folders that can't be opened for IMAP. 2025-02-16 16:17:41 +01:00
Burak Kaan Köse db833594f4 Make sure idle disconnects are not logged to app insights. 2025-02-16 16:14:50 +01:00
Burak Kaan Köse d36cf59829 Translated dates based on display language. (#567)
* Updating the app's culture based on the display language and making sure that dates/times are properly translated.
2025-02-16 14:46:34 +01:00
Aleh Khantsevich caae751698 Show "You" for active account in mail rendering page (#566)
* Added account contact view model to handle "You" case.

* fix namespaces again
2025-02-16 14:38:53 +01:00
Burak Kaan Köse f7836eedce Tracking failed imap setup steps for app insights. 2025-02-16 13:23:45 +01:00
Aleh Khantsevich 3ddc1a6229 file scoped namespaces (#565) 2025-02-16 11:54:23 +01:00
Burak Kaan Köse cf9869b71e Revert "File scoped namespaces"
This reverts commit d31d8f574e.
2025-02-16 11:43:30 +01:00
Aleh Khantsevich d31d8f574e File scoped namespaces 2025-02-16 11:35:43 +01:00
Burak Kaan Köse c1336428dc AppCenter to AppInsights migration. (#562)
* Remove AppCenter usage and libraries.

* Remove redundant pacakges and add the app insights sink.

* Diagnostic id support and manipulating telemetries.

* Handling of appdomain unhandled exceptions.

* Remove unused package identity package from mail project.

* Fixing printing.
2025-02-16 01:44:41 +01:00
Burak Kaan Köse f0e513bf0d New Crowdin updates (#559)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)
2025-02-15 12:55:45 +01:00
Burak Kaan Köse ee9e41c5a7 IMAP Improvements (#558)
* Fixing an issue where scrollviewer overrides a part of template in mail list. Adjusted zoomed out header grid's corner radius.

* IDLE implementation, imap synchronization strategies basics and condstore synchronization.

* Adding iCloud and Yahoo as special IMAP handling scenario.

* iCloud special imap handling.

* Support for killing synchronizers.

* Update privacy policy url.

* Batching condstore downloads into 50, using SORT extension for searches if supported.

* Bumping some nugets. More on the imap synchronizers.

* Delegating idle synchronizations to server to post-sync operations.

* Update mailkit to resolve qresync bug with iCloud.

* Fixing remote highest mode seq checks for qresync and condstore synchronizers.

* Yahoo custom settings.

* Bump google sdk package.

* Fixing the build issue....

* NRE on canceled token accounts during setup.

* Server crash handlers.

* Remove ARM32. Upgrade server to .NET 9.

* Fix icons for yahoo and apple.

* Fixed an issue where disabled folders causing an exception on forced sync.

* Remove smtp encoding constraint.

* Remove commented code.

* Fixing merge conflict

* Addressing double registrations for mailkit remote folder events in synchronizers.

* Making sure idle canceled result is not reported.

* Fixing custom imap server dialog opening.

* Fixing the issue with account creation making the previously selected account as selected as well.

* Fixing app close behavior and logging app close.
2025-02-15 12:53:32 +01:00
Aleh Khantsevich 30f1257983 Attempt to fix source generator issues (#556)
* Changed some properties of source generator project

* Remove accelerate
2025-02-14 19:16:54 +01:00
Aleh Khantsevich f007cef208 Updated Privacy policy URL (#557) 2025-02-14 19:15:42 +01:00
Burak Kaan Köse 19b5852098 Missing package description and fixing typo. 2025-02-14 02:14:04 +01:00
Aleh Khantsevich 2ec05ea7cc UWP .NET9 (#555)
* Ground work for NET9 UWP switch.

* Add launch settings for Wino.Mail

* Added new test WAP project

* fix platforms in slnx solution

* ManagePackageVersionsCentrally set default

* Fixing assets and couple issues with the new packaging project.

* Add back markdown

* Fix nuget warnings

* FIx error in WAP about build tools

* Add build.props with default language preview

* Some AOT compilation progress.

* More AOT stuff.

* Remove deprecated protocol auth activation handler.

* Fix remaining protocol handler for google auth.

* Even more AOT

* More more AOT fixes

* Fix a few more AOT warnings

* Fix signature editor AOT

* Fix composer and renderer AOT JSON

* Outlook Sync AOT

* Fixing bundle generation and package signing.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-02-14 01:43:52 +01:00
Aleh Khantsevich e8dd8bff44 Added save of drafts when closing app (#546) 2025-02-09 10:42:51 +01:00
Aleh Khantsevich ab3f65edfa clear selection on htmlRender (#544) 2025-02-04 21:47:49 +01:00
1912 changed files with 137890 additions and 55479 deletions
+7 -2
View File
@@ -8,6 +8,9 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
[XamlTypeInfo.g.cs]
dotnet_diagnostic.CS0612.severity = none
dotnet_diagnostic.CS0618.severity = none
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
@@ -149,7 +152,7 @@ csharp_preferred_modifier_order = public,private,protected,internal,static,exter
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_namespace_declarations = file_scoped:error
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
@@ -287,4 +290,6 @@ csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_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
+191
View File
@@ -0,0 +1,191 @@
# 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}"`
- Register control events (for example `Loaded`, `Unloaded`, `SizeChanged`, `PointerEntered`) in XAML markup, not with `+=` in `.xaml.cs`.
### 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)
+187
View File
@@ -0,0 +1,187 @@
name: Manual Beta Release
on:
workflow_dispatch:
inputs:
release_title:
description: Optional GitHub release title override
required: false
type: string
permissions:
contents: write
packages: read
jobs:
release-beta:
name: Build and publish beta release
runs-on: windows-latest
env:
PROJECT_PATH: Wino.Mail.WinUI/Wino.Mail.WinUI.csproj
MANIFEST_PATH: Wino.Mail.WinUI/Package.appxmanifest
CHANGELOG_PATH: CHANGELOG.md
NUGET_CONFIG_PATH: ${{ github.workspace }}\nuget.config
PACKAGE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\package
RELEASE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\release
CERTIFICATE_PFX_PATH: ${{ github.workspace }}\artifacts\signing\beta-signing-cert.pfx
CERTIFICATE_CER_PATH: ${{ github.workspace }}\artifacts\release\Wino-Mail-Beta.cer
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Fetch tags from origin
shell: pwsh
run: git fetch origin --force --tags
- name: Validate release secrets
shell: pwsh
env:
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
run: |
if ([string]::IsNullOrWhiteSpace($env:BETA_SIGNING_CERT_PFX_BASE64)) {
throw "Missing required secret: BETA_SIGNING_CERT_PFX_BASE64"
}
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
env:
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Compute beta version and release metadata
id: metadata
shell: pwsh
env:
RELEASE_TITLE_INPUT: ${{ github.event.inputs.release_title }}
run: |
$manifestPath = Join-Path $env:GITHUB_WORKSPACE $env:MANIFEST_PATH
if (-not (Test-Path $manifestPath)) {
throw "Package manifest not found: $manifestPath"
}
$changelogPath = Join-Path $env:GITHUB_WORKSPACE $env:CHANGELOG_PATH
if (-not (Test-Path $changelogPath)) {
throw "Release notes file not found: $changelogPath"
}
[xml]$manifest = Get-Content -LiteralPath $manifestPath
$identityNode = $manifest.Package.Identity
if (-not $identityNode) {
throw "Could not locate the Package/Identity node in $manifestPath"
}
$currentVersionText = [string]$identityNode.Version
if ($currentVersionText -notmatch '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)\.(?<revision>\d+)$') {
throw "Manifest version '$currentVersionText' is not a four-part numeric version."
}
$packageVersion = $currentVersionText
$releaseTag = "v$packageVersion"
$releaseTitleInput = $env:RELEASE_TITLE_INPUT
$releaseTitle = if ([string]::IsNullOrWhiteSpace($releaseTitleInput)) { $releaseTag } else { $releaseTitleInput.Trim() }
$headSha = (git rev-parse HEAD).Trim()
if ([string]::IsNullOrWhiteSpace($headSha)) {
throw "Failed to resolve the checked out commit SHA."
}
$notesInput = Get-Content -LiteralPath $changelogPath -Raw
if ([string]::IsNullOrWhiteSpace($notesInput)) {
throw "Release notes file is empty: $changelogPath"
}
$notesInput = $notesInput.Trim()
New-Item -ItemType Directory -Path $env:RELEASE_OUTPUT_DIR -Force | Out-Null
$releaseNotesPath = Join-Path $env:RELEASE_OUTPUT_DIR 'beta-release-notes.md'
$notesInput | Set-Content -LiteralPath $releaseNotesPath -Encoding utf8
"package_version=$packageVersion" >> $env:GITHUB_OUTPUT
"release_tag=$releaseTag" >> $env:GITHUB_OUTPUT
"release_title=$releaseTitle" >> $env:GITHUB_OUTPUT
"release_notes_path=$releaseNotesPath" >> $env:GITHUB_OUTPUT
"head_sha=$headSha" >> $env:GITHUB_OUTPUT
- name: Materialize signing certificate
shell: pwsh
env:
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
run: |
$signingDir = Split-Path -Parent $env:CERTIFICATE_PFX_PATH
New-Item -ItemType Directory -Path $signingDir -Force | Out-Null
[IO.File]::WriteAllBytes($env:CERTIFICATE_PFX_PATH, [Convert]::FromBase64String($env:BETA_SIGNING_CERT_PFX_BASE64))
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($env:CERTIFICATE_PFX_PATH, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
New-Item -ItemType Directory -Path (Split-Path -Parent $env:CERTIFICATE_CER_PATH) -Force | Out-Null
[IO.File]::WriteAllBytes($env:CERTIFICATE_CER_PATH, $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
- name: Restore WinUI project dependencies
shell: pwsh
run: |
if (-not (Test-Path $env:NUGET_CONFIG_PATH)) {
throw "NuGet config file not found: $env:NUGET_CONFIG_PATH"
}
dotnet restore $env:PROJECT_PATH `
--configfile $env:NUGET_CONFIG_PATH `
-p:Platform=x64 `
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH"
- name: Build MSIX bundle
shell: pwsh
run: |
New-Item -ItemType Directory -Path $env:PACKAGE_OUTPUT_DIR -Force | Out-Null
dotnet build $env:PROJECT_PATH `
--configuration Release `
--no-restore `
--configfile $env:NUGET_CONFIG_PATH `
/p:Platform=x64 `
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH" `
/p:GenerateAppxPackageOnBuild=true `
/p:UapAppxPackageBuildMode=SideloadOnly `
/p:AppxBundle=Always `
/p:AppxBundlePlatforms="x86|x64|arm64" `
/p:AppxPackageDir="$env:PACKAGE_OUTPUT_DIR\\" `
/p:AppxPackageVersion=${{ steps.metadata.outputs.package_version }} `
/p:PackageCertificateKeyFile="$env:CERTIFICATE_PFX_PATH" `
/p:PackageCertificatePassword= `
/p:PackageCertificateThumbprint= `
/p:AppxPackageSigningEnabled=true
- name: Collect packaged artifacts
id: package
shell: pwsh
run: |
$bundle = Get-ChildItem -Path $env:PACKAGE_OUTPUT_DIR -Recurse -Filter *.msixbundle | Select-Object -First 1
if (-not $bundle) {
throw "No .msixbundle file was generated under $env:PACKAGE_OUTPUT_DIR"
}
$releaseAssetPath = Join-Path $env:RELEASE_OUTPUT_DIR "Wino_${{ steps.metadata.outputs.package_version }}.zip"
if (Test-Path $releaseAssetPath) {
Remove-Item -LiteralPath $releaseAssetPath -Force
}
Compress-Archive -LiteralPath @($bundle.FullName, $env:CERTIFICATE_CER_PATH) -DestinationPath $releaseAssetPath -Force
"bundle_path=$($bundle.FullName)" >> $env:GITHUB_OUTPUT
"bundle_name=$($bundle.Name)" >> $env:GITHUB_OUTPUT
"release_asset_path=$releaseAssetPath" >> $env:GITHUB_OUTPUT
- name: Create GitHub prerelease
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.metadata.outputs.release_tag }}" `
"${{ steps.package.outputs.release_asset_path }}" `
--repo "${{ github.repository }}" `
--target "${{ steps.metadata.outputs.head_sha }}" `
--title "${{ steps.metadata.outputs.release_title }}" `
--notes-file "${{ steps.metadata.outputs.release_notes_path }}" `
--prerelease
+1
View File
@@ -399,3 +399,4 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml
Wino/obj/x86/Debug/XamlSaveStateFile.xml
*.cache
.vs/Wino/v16/.suo
/.claude/settings.local.json
+163
View File
@@ -0,0 +1,163 @@
# AGENTS.md
This file provides guidance to AI agent 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 WinUI project (Debug x64)
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
# Build WinUI project with diagnostic XAML/compiler logging (use when plain build only shows "XamlCompiler.exe exited with code 1")
dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false "/flp:logfile=winui-build.log;verbosity=diagnostic" /bl:winui-build.binlog
# Run tests (Debug x64)
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64
# Copilot CLI build command (Debug x64)
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
```
**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
**Startup project:** Wino.Mail.WinUI
**Platforms:** x86, x64, ARM64
## Efficient Workflow
- Start with targeted symbol or file search before reading full files
- Prefer one focused task per thread; use a new thread for unrelated follow-up work
- Keep verification narrow: build only the affected project, not the full solution, unless cross-project changes require it
- After the first restore, prefer `--no-restore` builds unless package or project references changed
- Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context
- When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository
- If a WinUI build only reports `XamlCompiler.exe exited with code 1`, rerun with the diagnostic logging command above and inspect the terminal output plus `winui-build.log` for real `WMC`/`WMC1121`/binding diagnostics before guessing
## 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)}`
- `x:Bind` does not implicitly convert `double` to `GridLength`; when binding `RowDefinition.Height` or `ColumnDefinition.Width`, use a `XamlHelpers` method such as `DoubleToGridLength(...)`
- For `ComboBox` controls in XAML, never use `DisplayMemberPath` or `SelectedValuePath`; use a typed `ItemTemplate` and bind `SelectedItem` explicitly, preferably with `x:Bind`
## 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 any resources.json file outside Wino.Core.Domain/Translations/en_US/resources.json
5. Treat all non-en_US translation files as managed externally and leave them untouched, even when adding new localization keys
6. In XAML, translation bindings must use `Mode=OneTime` because `Wino.Core.Domain/Translator.cs` does not implement `INotifyPropertyChanged`
## 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)
- Putting authentication validation, token refresh, account API calls, settings serialization/deserialization, or preference-application logic into ViewModels instead of the corresponding service
## 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
- For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations.
- When a `[RelayCommand]` needs enable/disable logic, prefer the command's `CanExecute` over binding `Button.IsEnabled` in XAML; use `[NotifyCanExecuteChangedFor]` on dependent properties and call `NotifyCanExecuteChanged()` explicitly when non-generated state affects the command.
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
- `ConfigureAwait(false)` continues execution on a background thread. Any UI-bound property change, `INotifyPropertyChanged` notification, collection mutation, or similar UI-facing state update after that point must be marshaled back with `ExecuteUIThread(...)` or the appropriate dispatcher call, otherwise the app can crash.
- Messenger messages are raised from a background thread by default, while UI control event handlers such as `Button.Click` start on the UI thread. Be deliberate when combining dispatcher usage with `ConfigureAwait(false)` so post-await UI updates always return to the UI thread.
- ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`.
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML.
- Never add XAML-backed UI controls to `.xaml.cs`. If a view has XAML, all control declarations, flyouts, templates, and visual composition belong in the `.xaml` file; keep `.xaml.cs` limited to event handling and view glue.
- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead.
- If you use `x:Load` in XAML, always give that `UIElement` an `x:Name`.
+7
View File
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
<IsAotCompatible>true</IsAotCompatible>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
</Project>
+80
View File
@@ -0,0 +1,80 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="ColorHashSharp" Version="1.1.0" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.2" />
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.2" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<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="CommunityToolkit.WinUI.Lottie" Version="8.2.250604" />
<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="5.2.1" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.Graph" Version="5.103.0" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.83.3" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.83.3" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.3" />
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.260316102" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.18" />
<PackageVersion Include="MimeKit" Version="4.15.1" />
<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.3.1" />
<PackageVersion Include="Sentry.Serilog" Version="6.3.1" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<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.2" />
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.2" />
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.5" />
<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.5" />
<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.4073" />
<PackageVersion Include="Google.Apis.Drive.v3" Version="1.73.0.4098" />
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.73.0.4029" />
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.72.0.3973" />
<PackageVersion Include="HtmlKit" Version="1.2.0" />
<PackageVersion Include="MailKit" Version="4.15.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.5" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.5" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<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="18.4.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="FluentAssertions" Version="8.9.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
</ItemGroup>
</Project>
+30 -21
View File
@@ -1,45 +1,54 @@
<p align="center">
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true
&mode=full">
<img src="https://www.winomail.app/images/wino_logo.png" width=90 height=90>
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true&mode=full">
<img src="https://www.winomail.app/images/v2/Logo.png" width="90" height="90" alt="Wino Mail logo">
</a>
<h3 align="center">Wino Mail</h3>
<p align="center">
Native mail client for Windows device families.
Native mail and calendar client for Windows.
</p>
</p>
<br>
![pdark](https://user-images.githubusercontent.com/12009960/232114528-2d2c8e3c-dbe7-429a-94e0-6aecc73bdf70.png)
![Wino Mail screenshot](https://user-images.githubusercontent.com/12009960/232114528-2d2c8e3c-dbe7-429a-94e0-6aecc73bdf70.png)
## Motivation
I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars
I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars.
## vNext Release Highlights
Wino vNext focuses on making Mail, Calendar, and Contacts feel like one cohesive native Windows experience while improving sync reliability and startup responsiveness.
- 📅 **Calendar management:** Event compose/create flow, calendar-mail mapping, reminder snooze support, occurrence and detail-page improvements, and CalDAV correctness fixes.
- 👥 **Contact management:** Improved contact workflows, account/settings integration, and contact data-model cleanup.
- 🔄 **Synchronization reliability:** Refactored synchronizers, better state handling, 404 + 429 error handling, and duplicate-operation prevention.
- ✉️ **Compose and drafts:** Refined editor/toolbar architecture, better rendering pipeline, Gmail draft support, and large Outlook attachment upload sessions.
-**Performance and quality:** Faster mail fetching with batched DB queries and caching, SQLite indexing/foreign key enforcement, and broader test + CI coverage.
- 🎨 **WinUI polish:** Improved onboarding/startup, settings and dialogs refresh, notification routing fixes, and keyboard/navigation quality-of-life improvements.
## Features
- API integration for Outlook and Gmail
- IMAP/SMTP support for custom mail servers
- Send, receive, mark as (read,important,spam etc), move mails.
- Linked/Merged Accounts
- Toast notifications with background sync.
- Instant startup performance
- Offline use / search.
- Modern and responsive UI
- Lots of personalization options
- Dark / Light mode for mail reader
- 📨 Outlook and Gmail API integration
- 🌐 IMAP/SMTP support for custom mail servers
- 📅 Calendar support with event creation/compose and reminders
- 👥 Contact management and people-centric account experience
- ✅ Core mail actions: send, receive, read/unread, move, spam, and more
- 🔗 Linked/Merged accounts
- 🔔 Toast notifications with background sync
- ⚡ Instant startup-oriented architecture
- 🔎 Offline-capable workflows and search improvements
- 🎛️ Modern responsive WinUI interface with personalization options
- 🌗 Dark/Light mode for mail reader and app surfaces
## Download
Download latest version of Wino Mail from Microsoft Store for free.
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true
&mode=full">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true&mode=full">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200" alt="Get Wino Mail from Microsoft Store"/>
</a>
## Beta Releases
@@ -48,7 +57,6 @@ Stable releases will always be distributed on Microsoft Store. However, beta rel
These releases are distributed as side-loaded packages. To install them, download the **.msixbundle** file in GitHub releases and [follow the steps explained here.](https://learn.microsoft.com/en-us/windows/application-management/sideload-apps-in-windows)
## Contributing
Check out the [contribution guidelines](/CONTRIBUTING.md) before diving into the source code or opening an issue. There are multiple ways to contribute and all of them are explained in detail there.
@@ -59,3 +67,4 @@ Your donations will motivate me more to work on Wino in my spare time and cover
- You can [donate via Paypal by clicking here](https://www.paypal.com/donate/?hosted_button_id=LGPERGGXFMQ7U)
- You can buy Unlimited Accounts add-on in the application. It's a one-time payment for lifetime, not a monthly recurring payment.
+158
View File
@@ -0,0 +1,158 @@
# Wino Mail vNext Improvements
This document summarizes the major improvements on `feature/vNext` compared to `main`, based on the commit history between the current branch and the merge-base with `main`.
## Wino Calendar
Calendar has grown from an early implementation into a much more complete product area on this branch.
### A full Wino Calendar experience
- Added a dedicated Wino Calendar app entry, making calendar a first-class experience instead of a secondary add-on.
- Built out the calendar rendering experience with multiple rounds of rendering improvements, updated calendar view styling, calendar buttons, and better event visuals.
- Added event creation and full event compose flows, including follow-up improvements for attachments, attendees, recurrence summaries, RSVP actions, reminders, and event details.
- Improved support for all-day events, better display dates, occurrence handling, and mail-to-calendar mapping so calendar actions connect more naturally with messages and invitations.
### Local calendar support
- Added local calendar operation coverage and supporting behavior for IMAP-backed/local calendar scenarios.
- Prevented duplicate operations by ignoring local calendar apply-changes in the wrong paths.
- Added busy-state support and metadata fetch flows so newly created accounts can initialize calendar data more reliably.
### CalDAV sync
- Introduced a dedicated CalDAV synchronizer and supporting service/client work.
- Fixed CalDAV delta sync issues.
- Fixed CalDAV timezone issues.
- Added manual live CalDAV workflow tests to validate real-world sync behavior.
This means local and self-hosted calendar scenarios are much better represented on this branch than on `main`.
### API calendar sync for Outlook and Gmail
- Expanded Outlook calendar sync behavior, including broader sync windows and fixes around date/time handling.
- Improved Gmail drafting and mail/calendar integration so event-related actions work better across providers.
- Added mail and calendar synchronizer state tracking to make sync progress and error handling more reliable.
- Added auto calendar sync on account creation and broader auto-sync trigger and cancellation support.
### Calendar polish and reliability
- Fixed calendar crashes and null-handling issues in calendar view date range updates.
- Fixed double initialization in calendar day views.
- Improved reaction to calendar changes and calendar item update-source handling.
- Added reminder snooze support across toast UI, services, and database storage.
Overall, Wino Calendar is one of the biggest themes of this branch: richer UI, more complete event workflows, and real sync support across local, CalDAV, Outlook, and Gmail-backed scenarios.
## Wino Accounts
Wino Accounts was significantly expanded and polished on this branch.
### Account flows and identity
- Added sign in, sign out, and registration flows.
- Redesigned login and registration dialogs.
- Added privacy policy presentation during registration.
- Added forgot password and email confirmation flows.
- Pointed the app to the real API and improved profile caching.
### Account management and settings
- Added Wino account settings and a dedicated management page.
- Added a special navigation item for Wino Accounts.
- Added import functionality for Wino Accounts.
- Added a preference to hide the title bar Wino account button.
- Improved the top-shell account icon and signed-out identity visuals.
### Purchases and add-ons
- Added handling for Paddle purchases and add-ons.
- Added purchase-success deep linking.
- Added support for AI pack handling through the Microsoft Store.
### User-facing polish
- Redesigned the Wino Account flyout and menu with a more polished Fluent-style presentation.
- Improved account cleanup behavior when an account is deleted.
- Added account attention handling and better account details/settings behavior.
Compared to `main`, this branch turns Wino Accounts into a much more complete platform feature rather than a minimal sign-in surface.
## Improved Stability and Reliability
A large part of this branch is about making the app more dependable in everyday use.
### Synchronization stability
- Refactored synchronizers to address long-standing reliability issues.
- Improved thread mapping across synchronizers.
- Added generic 404 handling for synchronizers.
- Added specific Outlook 429 handling for rate-limit scenarios.
- Improved Outlook authentication and Outlook sync reliability.
- Improved Gmail synchronizer behavior.
- Added explicit mail and calendar synchronizer state support.
### Mail and data reliability
- Optimized mail fetching with batched database queries and in-memory caching.
- Added SQLite indexes and enabled foreign key enforcement.
- Switched away from the old mail item queue approach and returned to a simpler initial sync strategy.
- Improved local draft resend behavior and added grace-period handling for local drafts.
- Added better handling for large Outlook attachments via upload sessions.
- Fixed issues with sent/draft placement, loading mails with infinite scroll, selection cleanup, and deleted-object scenarios.
### UI and lifecycle stability
- Fixed mail rendering page disposal issues.
- Fixed WebView2 runtime toast dispatching on the UI thread.
- Fixed startup mode issues, single-instancing problems, and shell/navigation regressions.
- Fixed multiple thread selection, container, flicker, and context-menu issues.
- Fixed crashes and null-reference style issues in several calendar and shell flows.
### Engineering quality
- Added more tests across calendar, CalDAV, IMAP, view-model, sanitization, and account sync scenarios.
- Added a GitHub Actions workflow to build WinUI and run Core tests on pull requests.
- Resolved warnings and moved the WinUI project toward warnings-as-errors discipline.
- Added AOT compatibility work and related cleanup across the app.
The branch is not just adding features; it is also clearly reducing failure points throughout sync, rendering, navigation, and storage.
## Contacts, Settings, and General UX
This branch also improves the everyday product experience outside mail and calendar core flows.
### Contacts
- Added contacts management.
- Improved contacts UI and related thread/image preview behavior.
- Removed legacy SQLite base64 contact storage from `AccountContact`.
- Added contact picture handling support and supporting contact service improvements.
### Settings
- Added a dedicated settings shell and refactored settings home/navigation.
- Expanded settings UI and introduced new setting options.
- Added calendar settings into the settings experience.
- Improved account details/settings pages and storage settings navigation.
- Refined settings visuals, shell integration, and menu behavior.
### Onboarding and app experience
- Added a new startup window and a more guided onboarding flow with wizard-like steps.
- Added a "What's New" implementation for feature communication.
- Improved dialogs, title bar behavior, shell content, navigation, and shell polish across multiple iterations.
- Added live store update notifications.
- Improved keyboard shortcuts and related dialogs.
- Added tray icon support and better toast routing between mail and calendar app entries.
## Summary
Compared to `main`, `feature/vNext` delivers four major leaps:
1. Wino Calendar becomes a substantially more complete feature set, including local calendar support, CalDAV sync, and stronger Outlook and Gmail calendar integration.
2. Wino Accounts becomes a real product surface with better authentication flows, management, imports, purchases, and polish.
3. The app is more stable thanks to synchronization refactors, storage improvements, test expansion, and many crash and lifecycle fixes.
4. Contacts, settings, onboarding, and shell/navigation experience all feel more mature and more consistent.
In short, this branch is a broad product maturation release rather than a narrow feature drop.
+8 -9
View File
@@ -1,17 +1,16 @@
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Authentication
namespace Wino.Authentication;
public abstract class BaseAuthenticator
{
public abstract class BaseAuthenticator
public abstract MailProviderType ProviderType { get; }
protected IAuthenticatorConfig AuthenticatorConfig { get; }
protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
{
public abstract MailProviderType ProviderType { get; }
protected IAuthenticatorConfig AuthenticatorConfig { get; }
protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
{
AuthenticatorConfig = authenticatorConfig;
}
AuthenticatorConfig = authenticatorConfig;
}
}
+32 -33
View File
@@ -7,45 +7,44 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication
namespace Wino.Authentication;
public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
{
public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig)
{
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);
}
public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId;
public bool ProposeCopyAuthURL { get; set; }
return new TokenInformationEx(userCredential.Token.AccessToken, account.Address);
}
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)
private Task<UserCredential> GetGoogleUserCredentialAsync(MailAccount account)
{
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
{
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));
}
ClientId = ClientId
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
}
}
@@ -1,16 +0,0 @@
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Authentication
{
public class Office365Authenticator : OutlookAuthenticator
{
public Office365Authenticator(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig) : base(nativeAppService, applicationConfiguration, authenticatorConfig)
{
}
public override MailProviderType ProviderType => MailProviderType.Office365;
}
}
+106 -83
View File
@@ -11,116 +11,139 @@ using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication
namespace Wino.Authentication;
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
{
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)
{
private const string TokenCacheFileName = "OutlookCache.bin";
private bool isTokenCacheAttached = false;
_nativeAppService = nativeAppService;
_applicationConfiguration = applicationConfiguration;
// Outlook
private const string Authority = "https://login.microsoftonline.com/common";
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
public override MailProviderType ProviderType => MailProviderType.Outlook;
private readonly IPublicClientApplication _publicClientApplication;
private readonly IApplicationConfiguration _applicationConfiguration;
public OutlookAuthenticator(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
_applicationConfiguration = applicationConfiguration;
Title = "Wino Mail",
ListOperatingSystemAccounts = true,
};
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
PublicClientApplicationBuilder outlookAppBuilder = null;
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
Title = "Wino Mail",
ListOperatingSystemAccounts = true,
};
var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
// 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;
_publicClientApplication = outlookAppBuilder.Build();
}
private async Task EnsureTokenCacheAttachedAsync()
public string[] Scope => AuthenticatorConfig.OutlookScope;
private async Task EnsureTokenCacheAttachedAsync()
{
if (!isTokenCacheAttached)
{
if (!isTokenCacheAttached)
{
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
isTokenCacheAttached = true;
}
isTokenCacheAttached = true;
}
}
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
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();
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address);
// 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 (storedAccount == null)
return await GenerateTokenInformationAsync(account);
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
try
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))
{
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
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 (MsalUiRequiredException)
{
// Somehow MSAL is not able to refresh the token silently.
// Force interactive login.
return await GenerateTokenInformationAsync(account);
}
catch (Exception)
{
throw;
}
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
catch (MsalClientException msalClientException)
{
try
{
await EnsureTokenCacheAttachedAsync();
if (msalClientException.ErrorCode == "authentication_canceled" || msalClientException.ErrorCode == "access_denied")
throw new AccountSetupCanceledException();
var 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.");
}
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));
throw;
}
throw new AuthenticationException(Translator.Exception_UnknowErrorDuringAuthentication, new Exception(Translator.Exception_TokenGenerationFailed));
}
}
+16 -16
View File
@@ -1,26 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<RootNamespace>Wino.Authentication</RootNamespace>
<Configurations>Debug;Release</Configurations>
<LangVersion>12</LangVersion>
<Platforms>AnyCPU;x64;x86</Platforms>
<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" Version="8.3.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="Google.Apis.Auth" Version="1.68.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.66.2" />
<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" />
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup>
</Project>
</Project>
-34
View File
@@ -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,129 +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|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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Identity.Client">
<Version>4.66.2</Version>
</PackageReference>
<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)' &lt; '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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

@@ -1,90 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.0.0" />
<Properties>
<DisplayName>Wino Calendar</DisplayName>
<PublisherDisplayName>Burak KÖSE</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" MaxVersionTested="10.0.14393.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Wino Calendar"
Description="Wino.Calendar.Packaging"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" />
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<!-- Registration of full trust backend application. -->
<uap:Extension Category="windows.appService">
<uap:AppService Name="WinoInteropService" />
</uap:Extension>
<!-- Protocol activation: Google OAuth -->
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="google.pw.oauth2">
<uap:DisplayName>Wino Google Authentication Protocol</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
<!-- Protocol activation: Launch UWP app from Full Trust Process -->
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="wino.calendar.launch">
<uap:DisplayName>Wino Calendara Launcher Protocol</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
<!-- Startup Task -->
<uap5:Extension
Category="windows.startupTask"
Executable="Wino.Server\Wino.Server.exe"
EntryPoint="Windows.FullTrustApplication">
<uap5:StartupTask
TaskId="WinoServer"
Enabled="false"
DisplayName="Wino Mail" />
</uap5:Extension>
<desktop:Extension Category="windows.fullTrustProcess" Executable="Wino.Server\Wino.Server.exe">
<desktop:FullTrustProcess>
<desktop:ParameterGroup GroupId="WinoServer" Parameters="Calendar" />
</desktop:FullTrustProcess>
</desktop:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="confirmAppClose" />
</Capabilities>
</Package>
@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM">
<Configuration>Debug</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM">
<Configuration>Release</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|AnyCPU">
<Configuration>Debug</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|AnyCPU">
<Configuration>Release</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>7485b18c-f5ab-4abe-ba7f-05b6623c67c8</ProjectGuid>
<TargetPlatformVersion>10.0.22621.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<DefaultLanguage>en-US</DefaultLanguage>
<AppxPackageSigningEnabled>false</AppxPackageSigningEnabled>
<NoWarn>$(NoWarn);NU1702</NoWarn>
<EntryPointProjectUniqueName>..\Wino.Calendar\Wino.Calendar.csproj</EntryPointProjectUniqueName>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\StoreLogo.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
<None Include="Package.StoreAssociation.xml" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
<ItemGroup>
<PackageReference Include="Microsoft.Identity.Client" Version="4.66.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Calendar\Wino.Calendar.csproj" />
<ProjectReference Include="..\Wino.Server\Wino.Server.csproj" />
</ItemGroup>
</Project>
@@ -1,48 +0,0 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.UI;
namespace Wino.Calendar.ViewModels
{
public partial class AccountDetailsPageViewModel : CalendarBaseViewModel
{
private readonly IAccountService _accountService;
public AccountProviderDetailViewModel Account { get; private set; }
public ICalendarDialogService CalendarDialogService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
public AccountDetailsPageViewModel(ICalendarDialogService calendarDialogService, IAccountService accountService, IAccountCalendarStateService accountCalendarStateService)
{
CalendarDialogService = calendarDialogService;
_accountService = accountService;
AccountCalendarStateService = accountCalendarStateService;
}
[RelayCommand]
private async Task RenameAccount()
{
if (Account == null)
return;
var updatedAccount = await CalendarDialogService.ShowEditAccountDialogAsync(Account.Account);
if (updatedAccount != null)
{
await _accountService.UpdateAccountAsync(updatedAccount);
ReportUIChange(new AccountUpdatedMessage(updatedAccount));
}
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
}
}
}
@@ -1,153 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
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;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.ViewModels;
using Wino.Messaging.Server;
namespace Wino.Calendar.ViewModels
{
public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
{
private readonly IProviderService _providerService;
public AccountManagementViewModel(ICalendarDialogService dialogService,
IWinoServerConnectionManager winoServerConnectionManager,
INavigationService navigationService,
IAccountService accountService,
IProviderService providerService,
IStoreManagementService storeManagementService,
IAuthenticationProvider authenticationProvider,
IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
{
CalendarDialogService = dialogService;
_providerService = providerService;
}
public ICalendarDialogService CalendarDialogService { get; }
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
await InitializeAccountsAsync();
}
public override async Task InitializeAccountsAsync()
{
Accounts.Clear();
var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
await ExecuteUIThread(() =>
{
foreach (var account in accounts)
{
var accountDetails = GetAccountProviderDetails(account);
Accounts.Add(accountDetails);
}
});
await ManageStorePurchasesAsync().ConfigureAwait(false);
}
[RelayCommand]
private async Task AddNewAccountAsync()
{
if (IsAccountCreationBlocked)
{
var isPurchaseClicked = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_AccountLimitMessage, Translator.DialogMessage_AccountLimitTitle, Translator.Buttons_Purchase);
if (!isPurchaseClicked) return;
await PurchaseUnlimitedAccountAsync();
return;
}
var availableProviders = _providerService.GetAvailableProviders();
var accountCreationDialogResult = await DialogService.ShowAccountProviderSelectionDialogAsync(availableProviders);
if (accountCreationDialogResult == null) return;
var accountCreationCancellationTokenSource = new CancellationTokenSource();
var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType);
accountCreationDialog.ShowDialog(accountCreationCancellationTokenSource);
accountCreationDialog.State = AccountCreationDialogState.SigningIn;
// For OAuth authentications, we just generate token and assign it to the MailAccount.
var createdAccount = new MailAccount()
{
ProviderType = accountCreationDialogResult.ProviderType,
Name = accountCreationDialogResult.AccountName,
AccountColorHex = accountCreationDialogResult.AccountColorHex,
Id = Guid.NewGuid()
};
var tokenInformationResponse = await WinoServerConnectionManager
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
createdAccount,
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
if (accountCreationDialog.State == AccountCreationDialogState.Canceled)
throw new AccountSetupCanceledException();
tokenInformationResponse.ThrowIfFailed();
//var tokenInformation = tokenInformationResponse.Data;
//createdAccount.Address = tokenInformation.Address;
//tokenInformation.AccountId = createdAccount.Id;
await AccountService.CreateAccountAsync(createdAccount, null);
// Sync profile information if supported.
if (createdAccount.IsProfileInfoSyncSupported)
{
// Start profile information synchronization.
// It's only available for Outlook and Gmail synchronizers.
var profileSyncOptions = new MailSynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = MailSynchronizationType.UpdateProfile
};
var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
var profileSynchronizationResult = profileSynchronizationResponse.Data;
if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
}
accountCreationDialog.State = AccountCreationDialogState.FetchingEvents;
// Start synchronizing events.
var synchronizationOptions = new CalendarSynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = CalendarSynchronizationType.CalendarMetadata
};
var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
}
}
}
@@ -1,367 +0,0 @@
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;
namespace Wino.Calendar.ViewModels
{
public partial class AppShellViewModel : CalendarBaseViewModel,
IRecipient<VisibleDateRangeChangedMessage>,
IRecipient<CalendarEnableStatusChangedMessage>,
IRecipient<NavigateManageAccountsRequested>,
IRecipient<CalendarDisplayTypeChangedMessage>,
IRecipient<DetailsPageStateChangedMessage>
{
public IPreferencesService PreferencesService { get; }
public IStatePersistanceService StatePersistenceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
public INavigationService NavigationService { get; }
public IWinoServerConnectionManager ServerConnectionManager { get; }
[ObservableProperty]
private bool _isEventDetailsPageActive;
[ObservableProperty]
private int _selectedMenuItemIndex = -1;
[ObservableProperty]
private bool isCalendarEnabled;
/// <summary>
/// Gets or sets the active connection status of the Wino server.
/// </summary>
[ObservableProperty]
private WinoServerConnectionStatus activeConnectionStatus;
/// <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 AppShellViewModel(IPreferencesService preferencesService,
IStatePersistanceService statePersistanceService,
IAccountService accountService,
ICalendarService calendarService,
IAccountCalendarStateService accountCalendarStateService,
INavigationService navigationService,
IWinoServerConnectionManager serverConnectionManager)
{
_accountService = accountService;
_calendarService = calendarService;
AccountCalendarStateService = accountCalendarStateService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
NavigationService = navigationService;
ServerConnectionManager = serverConnectionManager;
PreferencesService = preferencesService;
StatePersistenceService = statePersistanceService;
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
}
private void SelectedCalendarItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
throw new NotImplementedException();
}
private void PrefefencesChanged(object sender, string e)
{
if (e == nameof(StatePersistenceService.CalendarDisplayType))
{
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
// Change the calendar.
DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate()));
}
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
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.ClearGroupedAccountCalendar());
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.CalendarMetadata
}, SynchronizationSource.Client);
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;
case CalendarDisplayType.Year:
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 Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync();
[RelayCommand]
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
{
_navigationDate = clickedDateArgs.ClickedDate;
ForceNavigateCalendarDate();
}
#endregion
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()
{
DateNavigationHeaderItems.Clear();
// TODO: From settings
var testInfo = new CultureInfo("en-US");
switch (StatePersistenceService.CalendarDisplayType)
{
case CalendarDisplayType.Day:
case CalendarDisplayType.Week:
case CalendarDisplayType.WorkWeek:
case CalendarDisplayType.Month:
DateNavigationHeaderItems.ReplaceRange(testInfo.DateTimeFormat.MonthNames);
break;
case CalendarDisplayType.Year:
break;
default:
break;
}
SetDateNavigationHeaderItems();
}
partial void OnHighlightedDateRangeChanged(DateRange value) => SetDateNavigationHeaderItems();
private void SetDateNavigationHeaderItems()
{
if (HighlightedDateRange == null) return;
if (DateNavigationHeaderItems.Count == 0)
{
UpdateDateNavigationHeaderItems();
}
// TODO: Year view
var monthIndex = HighlightedDateRange.GetMostVisibleMonthIndex();
SelectedDateNavigationHeaderIndex = Math.Max(monthIndex - 1, -1);
}
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));
public async void Receive(DetailsPageStateChangedMessage message)
{
await ExecuteUIThread(() =>
{
IsEventDetailsPageActive = message.IsActivated;
// TODO: This is for Wino Mail. Generalize this later on.
StatePersistenceService.IsReaderNarrowed = message.IsActivated;
StatePersistenceService.IsReadingMail = message.IsActivated;
});
}
}
}
@@ -0,0 +1,123 @@
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; }
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;
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
}
partial void OnAccountColorHexChanged(string value)
{
if (AccountCalendar != null && !string.IsNullOrEmpty(value))
{
AccountCalendar.BackgroundColorHex = value;
AccountCalendar.IsBackgroundColorUserOverridden = true;
SaveChangesAsync();
}
}
partial void OnIsSyncEnabledChanged(bool value)
{
if (AccountCalendar != null)
{
AccountCalendar.IsSynchronizationEnabled = 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,559 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
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;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.MenuItems;
using Wino.Core.Domain.Models;
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.Server;
using Wino.Messaging.UI;
namespace Wino.Calendar.ViewModels;
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
ICalendarShellClient,
IRecipient<CalendarDisplayTypeChangedMessage>,
IRecipient<AccountRemovedMessage>
{
public IPreferencesService PreferencesService { get; }
public IStatePersistanceService StatePersistenceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
public INavigationService NavigationService { get; }
public WinoApplicationMode Mode => WinoApplicationMode.Calendar;
public bool HandlesNavigationSelection => false;
public VisibleDateRange CurrentVisibleRange => _calendarPageViewModel.CurrentVisibleRange;
public string VisibleDateRangeText => _calendarPageViewModel.VisibleDateRangeText;
System.Collections.IEnumerable ICalendarShellClient.GroupedAccountCalendars => AccountCalendarStateService.GroupedAccountCalendars;
System.Collections.IEnumerable ICalendarShellClient.DateNavigationHeaderItems => DateNavigationHeaderItems;
object IShellClient.SelectedMenuItem
{
get => null;
set { }
}
System.Windows.Input.ICommand ICalendarShellClient.TodayClickedCommand => TodayClickedCommand;
System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand;
System.Windows.Input.ICommand ICalendarShellClient.PreviousDateRangeCommand => PreviousDateRangeCommand;
System.Windows.Input.ICommand ICalendarShellClient.NextDateRangeCommand => NextDateRangeCommand;
System.Windows.Input.ICommand ICalendarShellClient.SyncCommand => SyncCommand;
public bool CanSynchronizeCalendars => !AccountCalendarStateService.IsAnySynchronizationInProgress;
public MenuItemCollection MenuItems { get; private set; }
public MenuItemCollection FooterItems { get; private set; }
[ObservableProperty]
private int _selectedMenuItemIndex = -1;
[ObservableProperty]
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
[ObservableProperty]
private int _selectedDateNavigationHeaderIndex;
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
[ObservableProperty]
private bool isStoreUpdateItemVisible;
private readonly SettingsItem _settingsItem = new();
private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new();
private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
private readonly CalendarPageViewModel _calendarPageViewModel;
private readonly IMailDialogService _dialogService;
private readonly IStoreUpdateService _storeUpdateService;
private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService;
private readonly IDateContextProvider _dateContextProvider;
private bool _runtimeSubscriptionsAttached;
private bool _hasRegisteredPersistentRecipients;
private DateTime? _navigationDate;
public CalendarAppShellViewModel(
IPreferencesService preferencesService,
IStatePersistanceService statePersistanceService,
IAccountService accountService,
ICalendarService calendarService,
IAccountCalendarStateService accountCalendarStateService,
INavigationService navigationService,
CalendarPageViewModel calendarPageViewModel,
IMailDialogService dialogService,
IStoreUpdateService storeUpdateService,
IDateContextProvider dateContextProvider)
{
PreferencesService = preferencesService;
StatePersistenceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService;
NavigationService = navigationService;
_accountService = accountService;
_calendarService = calendarService;
_calendarPageViewModel = calendarPageViewModel;
_dialogService = dialogService;
_storeUpdateService = storeUpdateService;
_dateContextProvider = dateContextProvider;
_calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged;
AccountCalendarStateService.PropertyChanged += AccountCalendarStateServicePropertyChanged;
}
protected override void OnDispatcherAssigned()
{
base.OnDispatcherAssigned();
AccountCalendarStateService.Dispatcher = Dispatcher;
MenuItems = new MenuItemCollection(Dispatcher);
FooterItems = new MenuItemCollection(Dispatcher);
_ = RefreshFooterItemsAsync(false);
}
private void CalendarPageViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange))
{
OnPropertyChanged(nameof(CurrentVisibleRange));
}
if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange) ||
e.PropertyName == nameof(CalendarPageViewModel.VisibleDateRangeText))
{
OnPropertyChanged(nameof(VisibleDateRangeText));
UpdateDateNavigationHeaderItems();
}
}
private void PrefefencesChanged(object sender, string e)
{
if (e != nameof(StatePersistenceService.CalendarDisplayType))
return;
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
OnPropertyChanged(nameof(IsVerticalCalendar));
UpdateDateNavigationHeaderItems();
NavigateCalendarDate(GetDisplayTypeSwitchDate());
}
private async void PreferencesServiceChanged(object sender, string e)
{
if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
{
await RefreshFooterItemsAsync(false);
}
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
if (!_hasRegisteredPersistentRecipients)
{
RegisterRecipients();
_hasRegisteredPersistentRecipients = true;
}
AttachRuntimeSubscriptions();
var activationContext = parameters as ShellModeActivationContext;
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
var navigationArgs = activationContext?.Parameter as CalendarPageNavigationArgs;
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
PreferencesService.PreferenceChanged += PreferencesServiceChanged;
await RefreshFooterItemsAsync(mode == NavigationMode.New);
UpdateDateNavigationHeaderItems();
await InitializeAccountCalendarsAsync();
ValidateConfiguredNewEventCalendar();
if (navigationArgs != null)
{
NavigationService.Navigate(WinoPage.CalendarPage, navigationArgs);
}
else if (shouldRunStartupFlows || _calendarPageViewModel.CurrentVisibleRange == null)
{
TodayClicked();
}
}
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
DetachRuntimeSubscriptions();
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
_ = ExecuteUIThread(() =>
{
DateNavigationHeaderItems.Clear();
AccountCalendarStateService.ClearGroupedAccountCalendars();
SelectedDateNavigationHeaderIndex = -1;
});
_calendarPageViewModel.CleanupForShellDeactivation();
}
public void PrepareForShellShutdown()
{
DetachRuntimeSubscriptions();
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
if (_hasRegisteredPersistentRecipients)
{
UnregisterRecipients();
_hasRegisteredPersistentRecipients = false;
}
DateNavigationHeaderItems.Clear();
SelectedDateNavigationHeaderIndex = -1;
SelectedMenuItemIndex = -1;
MenuItems?.Clear();
FooterItems?.Clear();
AccountCalendarStateService.ClearGroupedAccountCalendars();
_calendarPageViewModel.CleanupForShellDeactivation();
}
private void AccountCalendarStateServicePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(IAccountCalendarStateService.IsAnySynchronizationInProgress))
return;
OnPropertyChanged(nameof(CanSynchronizeCalendars));
SyncCommand.NotifyCanExecuteChanged();
}
private void AttachRuntimeSubscriptions()
{
if (_runtimeSubscriptionsAttached)
return;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
_runtimeSubscriptionsAttached = true;
}
private void DetachRuntimeSubscriptions()
{
if (!_runtimeSubscriptionsAttached)
return;
AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged;
StatePersistenceService.StatePropertyChanged -= PrefefencesChanged;
_runtimeSubscriptionsAttached = false;
}
private async Task RefreshFooterItemsAsync(bool showNotification)
{
await ExecuteUIThread(() =>
{
FooterItems.Clear();
});
}
private async Task StartStoreUpdateAsync()
{
await _storeUpdateService.StartUpdateAsync().ConfigureAwait(false);
await RefreshFooterItemsAsync(false).ConfigureAwait(false);
}
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
{
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 = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList();
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
await Dispatcher.ExecuteOnUIThread(() =>
{
AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel);
});
}
}
private void NavigateCalendarDate(DateTime date)
{
_navigationDate = date.Date;
ForceNavigateCalendarDate();
}
private void ForceNavigateCalendarDate()
{
var args = new CalendarPageNavigationArgs
{
NavigationDate = _navigationDate ?? DateTime.Now.Date
};
NavigationService.Navigate(WinoPage.CalendarPage, args);
_navigationDate = null;
}
[RelayCommand(CanExecute = nameof(CanSynchronizeCalendars))]
private async Task Sync()
{
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
{
AccountId = account.Id,
Type = CalendarSynchronizationType.Strict
}));
}
}
private DateTime GetDisplayTypeSwitchDate()
{
var today = _dateContextProvider.GetToday();
var settings = PreferencesService.GetCurrentCalendarSettings();
var referenceRange = CurrentVisibleRange
?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
var targetRange = CalendarRangeResolver.ChangeDisplayType(referenceRange, StatePersistenceService.CalendarDisplayType, settings, today);
return targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue);
}
[RelayCommand]
private void TodayClicked()
{
NavigateCalendarDate(_dateContextProvider.GetToday().ToDateTime(TimeOnly.MinValue));
}
[RelayCommand]
private void PreviousDateRange()
{
NavigateRelativePeriod(-1);
}
[RelayCommand]
private void NextDateRange()
{
NavigateRelativePeriod(1);
}
private void NavigateRelativePeriod(int direction)
{
var today = _dateContextProvider.GetToday();
var settings = PreferencesService.GetCurrentCalendarSettings();
var referenceRange = CurrentVisibleRange
?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
var targetRange = CalendarRangeResolver.Navigate(referenceRange, direction, settings, today);
NavigateCalendarDate(targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue));
}
public async Task HandleNavigationItemInvokedAsync(IMenuItem menuItem)
{
switch (menuItem)
{
case NewMailMenuItem:
await NewEventAsync().ConfigureAwait(false);
break;
case SettingsItem:
NavigationService.Navigate(WinoPage.SettingsPage);
break;
case StoreUpdateMenuItem:
await StartStoreUpdateAsync().ConfigureAwait(false);
break;
}
}
[RelayCommand]
private async Task NewEventAsync()
{
var pickedCalendar = TryResolveConfiguredNewEventCalendar();
if (pickedCalendar == null)
{
var availableGroups = AccountCalendarStateService.GroupedAccountCalendars
.Where(group => group.AccountCalendars.Count > 0)
.Select(group => new CalendarPickerAccountGroup
{
Account = group.Account,
Calendars = group.AccountCalendars.Select(calendar => calendar.AccountCalendar).ToList()
})
.ToList();
if (availableGroups.Count == 0)
{
_dialogService.InfoBarMessage(
Translator.CalendarEventCompose_NoCalendarsTitle,
Translator.CalendarEventCompose_NoCalendarsMessage,
InfoBarMessageType.Warning);
return;
}
var pickingResult = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups);
if (pickingResult.ShouldNavigateToCalendarSettings)
{
NavigationService.Navigate(WinoPage.CalendarPreferenceSettingsPage);
return;
}
pickedCalendar = pickingResult.PickedCalendar;
}
if (pickedCalendar == null)
return;
var (startDate, endDate) = GetDefaultComposeDateRange();
NavigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
{
SelectedCalendarId = pickedCalendar.Id,
StartDate = startDate,
EndDate = endDate
});
}
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{
if (args.Handled || args.Mode != WinoApplicationMode.Calendar)
return;
if (args.Action == KeyboardShortcutAction.NewEvent)
{
await NewEventAsync();
args.Handled = true;
}
}
[RelayCommand]
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
=> NavigateCalendarDate(clickedDateArgs.ClickedDate);
protected override void RegisterRecipients()
{
base.RegisterRecipients();
UnregisterRecipients();
Messenger.Register<CalendarDisplayTypeChangedMessage>(this);
Messenger.Register<AccountRemovedMessage>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
Messenger.Unregister<CalendarDisplayTypeChangedMessage>(this);
Messenger.Unregister<AccountRemovedMessage>(this);
}
private void UpdateDateNavigationHeaderItems()
{
var headerText = VisibleDateRangeText;
DateNavigationHeaderItems.ReplaceRange(string.IsNullOrWhiteSpace(headerText) ? [] : [headerText]);
SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
}
public void Receive(CalendarDisplayTypeChangedMessage message)
{
OnPropertyChanged(nameof(IsVerticalCalendar));
UpdateDateNavigationHeaderItems();
}
public async void Receive(AccountRemovedMessage message)
{
await InitializeAccountCalendarsAsync();
ValidateConfiguredNewEventCalendar();
}
private AccountCalendar TryResolveConfiguredNewEventCalendar()
{
ValidateConfiguredNewEventCalendar();
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
{
return null;
}
return AccountCalendarStateService.AllCalendars
.FirstOrDefault(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value)?
.AccountCalendar;
}
private void ValidateConfiguredNewEventCalendar()
{
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
{
return;
}
var exists = AccountCalendarStateService.AllCalendars
.Any(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value);
if (!exists)
{
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
PreferencesService.DefaultNewEventCalendarId = null;
}
}
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
{
var localNow = DateTime.Now;
var roundedMinutes = localNow.Minute switch
{
< 30 => 30,
30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
_ => 60
};
var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
return (startDate, startDate.AddMinutes(30));
}
void IShellClient.Activate(ShellModeActivationContext activationContext)
=> OnNavigatedTo(NavigationMode.New, activationContext);
void IShellClient.Deactivate()
=> OnNavigatedFrom(NavigationMode.New, null!);
Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem)
=> menuItem == null ? Task.CompletedTask : HandleNavigationItemInvokedAsync(menuItem);
Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem)
=> Task.CompletedTask;
}
@@ -0,0 +1,760 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using EmailValidation;
using Wino.Calendar.ViewModels.Data;
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.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Validation;
using Wino.Core.ViewModels;
namespace Wino.Calendar.ViewModels;
public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel
{
private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
private readonly IMailDialogService _dialogService;
private readonly IContactService _contactService;
private readonly IPreferencesService _preferencesService;
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly CalendarEventComposeResultValidator _composeResultValidator = new();
public Func<Task<string>> GetHtmlNotesAsync { get; set; }
public ObservableCollection<AccountCalendarViewModel> AvailableCalendars { get; } = [];
public ObservableCollection<GroupedAccountCalendarViewModel> AvailableCalendarGroups { get; } = [];
public ObservableCollection<CalendarComposeAttendeeViewModel> Attendees { get; } = [];
public ObservableCollection<CalendarComposeAttachmentViewModel> Attachments { get; } = [];
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = [];
public ObservableCollection<ReminderOption> ReminderOptions { get; } = [];
public ObservableCollection<int> RecurrenceIntervalOptions { get; } = [];
public ObservableCollection<CalendarComposeFrequencyOption> RecurrenceFrequencyOptions { get; } = [];
public ObservableCollection<CalendarComposeWeekdayOption> WeekdayOptions { get; } = [];
[ObservableProperty]
public partial AccountCalendarViewModel SelectedCalendar { get; set; }
[ObservableProperty]
public partial string Title { get; set; } = string.Empty;
[ObservableProperty]
public partial string Location { get; set; } = string.Empty;
[ObservableProperty]
public partial bool IsAllDay { get; set; }
[ObservableProperty]
public partial DateTimeOffset StartDate { get; set; }
[ObservableProperty]
public partial TimeSpan StartTime { get; set; }
[ObservableProperty]
public partial TimeSpan EndTime { get; set; }
[ObservableProperty]
public partial DateTimeOffset AllDayEndDate { get; set; }
[ObservableProperty]
public partial bool IsRecurring { get; set; }
[ObservableProperty]
public partial int SelectedRecurrenceInterval { get; set; } = 1;
[ObservableProperty]
public partial CalendarComposeFrequencyOption SelectedRecurrenceFrequencyOption { get; set; }
[ObservableProperty]
public partial DateTimeOffset? RecurrenceEndDate { get; set; }
[ObservableProperty]
public partial string RecurrenceSummary { get; set; } = string.Empty;
[ObservableProperty]
public partial ReminderOption SelectedReminderOption { get; set; }
[ObservableProperty]
public partial ShowAsOption SelectedShowAsOption { get; set; }
[ObservableProperty]
public partial bool IsDarkWebviewRenderer { get; set; }
[ObservableProperty]
public partial CalendarEventComposeResult LastCreatedResult { get; set; }
public CalendarSettings CurrentSettings { get; }
public string TimePickerClockIdentifier => CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "24HourClock" : "12HourClock";
public bool HasAttachments => Attachments.Count > 0;
public bool IsSelectedCalendarCalDav => SelectedCalendar?.Account?.ProviderType == MailProviderType.IMAP4 &&
SelectedCalendar.Account.ServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.CalDav;
public bool CanAddAttachments => !IsSelectedCalendarCalDav;
public string AttachmentsDisabledTooltipText => IsSelectedCalendarCalDav
? Translator.CalendarEventCompose_AttachmentsNotSupportedForCalDav
: string.Empty;
public string SelectedCalendarDisplayText => SelectedCalendar?.Name ?? Translator.CalendarEventCompose_SelectCalendar;
public string SelectedCalendarAccountText => SelectedCalendar?.Account?.Address ?? string.Empty;
public bool IsDailyRecurrenceSelected => SelectedRecurrenceFrequencyOption?.Frequency == CalendarItemRecurrenceFrequency.Daily;
public CalendarEventComposePageViewModel(IAccountService accountService,
ICalendarService calendarService,
INavigationService navigationService,
IMailDialogService dialogService,
IContactService contactService,
IPreferencesService preferencesService,
IUnderlyingThemeService underlyingThemeService,
IWinoRequestDelegator winoRequestDelegator)
{
_accountService = accountService;
_calendarService = calendarService;
_navigationService = navigationService;
_dialogService = dialogService;
_contactService = contactService;
_preferencesService = preferencesService;
_underlyingThemeService = underlyingThemeService;
_winoRequestDelegator = winoRequestDelegator;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
Attachments.CollectionChanged += AttachmentsCollectionChanged;
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));
foreach (var reminderMinutes in _calendarService.GetPredefinedReminderMinutes().OrderByDescending(x => x))
{
ReminderOptions.Add(new ReminderOption(reminderMinutes));
}
foreach (var interval in Enumerable.Range(1, 99))
{
RecurrenceIntervalOptions.Add(interval);
}
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Daily, Translator.CalendarEventCompose_FrequencyDay));
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Weekly, Translator.CalendarEventCompose_FrequencyWeek));
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Monthly, Translator.CalendarEventCompose_FrequencyMonth));
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Yearly, Translator.CalendarEventCompose_FrequencyYear));
SelectedRecurrenceFrequencyOption = RecurrenceFrequencyOptions.FirstOrDefault();
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Monday, "MO", Translator.CalendarEventCompose_Weekday_Monday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Tuesday, "TU", Translator.CalendarEventCompose_Weekday_Tuesday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Wednesday, "WE", Translator.CalendarEventCompose_Weekday_Wednesday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Thursday, "TH", Translator.CalendarEventCompose_Weekday_Thursday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Friday, "FR", Translator.CalendarEventCompose_Weekday_Friday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Saturday, "SA", Translator.CalendarEventCompose_Weekday_Saturday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Sunday, "SU", Translator.CalendarEventCompose_Weekday_Sunday));
SelectedReminderOption = GetDefaultReminderOption();
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy);
var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
ApplyDateRange(defaultStart, defaultEnd, false);
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
await LoadAvailableCalendarsAsync();
var args = parameters as CalendarEventComposeNavigationArgs;
ApplyNavigationArgs(args);
UpdateRecurrenceSummary();
}
partial void OnSelectedCalendarChanged(AccountCalendarViewModel value)
{
if (value == null)
return;
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == value.DefaultShowAs)
?? ShowAsOptions.FirstOrDefault();
if (IsSelectedCalendarCalDav && Attachments.Count > 0)
{
Attachments.Clear();
}
OnPropertyChanged(nameof(IsSelectedCalendarCalDav));
OnPropertyChanged(nameof(CanAddAttachments));
OnPropertyChanged(nameof(AttachmentsDisabledTooltipText));
OnPropertyChanged(nameof(SelectedCalendarDisplayText));
OnPropertyChanged(nameof(SelectedCalendarAccountText));
}
partial void OnIsAllDayChanged(bool value)
{
if (value)
{
if (AllDayEndDate.Date <= StartDate.Date)
{
AllDayEndDate = StartDate.AddDays(1);
}
}
UpdateRecurrenceSummary();
}
partial void OnStartDateChanged(DateTimeOffset value)
{
if (IsAllDay && AllDayEndDate.Date <= value.Date)
{
AllDayEndDate = value.AddDays(1);
}
if (IsRecurring && WeekdayOptions.All(option => !option.IsSelected))
{
SelectSingleWeekday(value.DayOfWeek);
}
UpdateRecurrenceSummary();
}
partial void OnStartTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
partial void OnEndTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
partial void OnAllDayEndDateChanged(DateTimeOffset value) => UpdateRecurrenceSummary();
partial void OnIsRecurringChanged(bool value)
{
if (value && WeekdayOptions.All(option => !option.IsSelected))
{
SelectSingleWeekday(StartDate.DayOfWeek);
}
UpdateRecurrenceSummary();
}
partial void OnSelectedRecurrenceIntervalChanged(int value) => UpdateRecurrenceSummary();
partial void OnSelectedRecurrenceFrequencyOptionChanged(CalendarComposeFrequencyOption value)
{
OnPropertyChanged(nameof(IsDailyRecurrenceSelected));
UpdateRecurrenceSummary();
}
partial void OnRecurrenceEndDateChanged(DateTimeOffset? value) => UpdateRecurrenceSummary();
[RelayCommand]
private async Task AddAttachmentsAsync()
{
if (!CanAddAttachments)
return;
var pickedFiles = await _dialogService.PickFilesMetadataAsync("*");
if (pickedFiles.Count == 0)
return;
await ExecuteUIThread(() =>
{
foreach (var file in pickedFiles)
{
TryAddAttachment(file.FileName, file.FullFilePath, file.FileExtension, file.Size);
}
});
}
public bool TryAddAttachment(string filePath, long size)
{
if (string.IsNullOrWhiteSpace(filePath))
return false;
var fileName = Path.GetFileName(filePath);
var fileExtension = Path.GetExtension(filePath);
return TryAddAttachment(fileName, filePath, fileExtension, size);
}
[RelayCommand]
private void RemoveAttachment(CalendarComposeAttachmentViewModel attachment)
{
if (attachment == null)
return;
Attachments.Remove(attachment);
}
[RelayCommand]
private void ClearRecurrenceEndDate()
{
RecurrenceEndDate = null;
}
[RelayCommand]
private void Cancel()
{
_navigationService.GoBack();
}
[RelayCommand]
private async Task CreateAsync()
{
var uniqueAttendees = Attendees
.GroupBy(attendee => attendee.Email, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.ToList();
var createdResult = await BuildResultAsync(uniqueAttendees);
try
{
_composeResultValidator.Validate(createdResult);
}
catch (CalendarEventComposeValidationException ex)
{
ShowValidationMessage(ex.Message);
return;
}
LastCreatedResult = createdResult;
await _winoRequestDelegator.ExecuteAsync(new CalendarOperationPreparationRequest(
CalendarSynchronizerOperation.CreateEvent,
ComposeResult: createdResult));
NavigateBackToCalendar(createdResult.StartDate);
}
private void NavigateBackToCalendar(DateTime targetDate)
{
_navigationService.Navigate(
WinoPage.CalendarPage,
new CalendarPageNavigationArgs
{
NavigationDate = targetDate,
ForceReload = true
});
}
public async Task<List<AccountContact>> SearchContactsAsync(string queryText)
{
if (string.IsNullOrWhiteSpace(queryText) || queryText.Length < 2)
return [];
return await _contactService.GetAddressInformationAsync(queryText).ConfigureAwait(false);
}
public async Task<CalendarComposeAttendeeViewModel> GetAttendeeAsync(string tokenText)
{
if (!EmailValidator.Validate(tokenText))
return null;
var existing = Attendees.Any(attendee => attendee.Email.Equals(tokenText, StringComparison.OrdinalIgnoreCase));
if (existing)
return null;
var info = await _contactService.GetAddressInformationByAddressAsync(tokenText).ConfigureAwait(false);
if (info != null)
{
return CalendarComposeAttendeeViewModel.FromContact(info);
}
return new CalendarComposeAttendeeViewModel(string.Empty, tokenText);
}
public void AddAttendee(CalendarComposeAttendeeViewModel attendee)
{
if (Attendees.Any(existing => existing.Email.Equals(attendee.Email, StringComparison.OrdinalIgnoreCase)))
return;
Attendees.Add(attendee);
}
[RelayCommand]
private void RemoveAttendee(CalendarComposeAttendeeViewModel attendee)
{
if (attendee == null)
return;
Attendees.Remove(attendee);
}
public void NotifyAddressExists()
{
_dialogService.InfoBarMessage(
Translator.Info_ContactExistsTitle,
Translator.Info_ContactExistsMessage,
InfoBarMessageType.Warning);
}
public void NotifyInvalidEmail(string address)
{
_dialogService.InfoBarMessage(
Translator.Info_InvalidAddressTitle,
string.Format(Translator.Info_InvalidAddressMessage, address),
InfoBarMessageType.Warning);
}
private async Task LoadAvailableCalendarsAsync()
{
var accountCalendars = new List<AccountCalendarViewModel>();
var groupedCalendars = new List<GroupedAccountCalendarViewModel>();
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var viewModels = calendars
.Select(calendar => new AccountCalendarViewModel(account, calendar))
.ToList();
accountCalendars.AddRange(viewModels);
if (viewModels.Count > 0)
{
groupedCalendars.Add(new GroupedAccountCalendarViewModel(account, viewModels));
}
}
await ExecuteUIThread(() =>
{
AvailableCalendars.Clear();
AvailableCalendarGroups.Clear();
foreach (var calendar in accountCalendars.OrderBy(calendar => calendar.Account.Name).ThenBy(calendar => calendar.Name))
{
AvailableCalendars.Add(calendar);
}
foreach (var group in groupedCalendars.OrderBy(group => group.Account.Name))
{
AvailableCalendarGroups.Add(group);
}
});
}
private void ApplyNavigationArgs(CalendarEventComposeNavigationArgs args)
{
var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
var startDate = args?.StartDate != default ? args!.StartDate : defaultStart;
var endDate = args?.EndDate != default ? args!.EndDate : defaultEnd;
var isAllDay = args?.IsAllDay ?? false;
Title = args?.Title ?? string.Empty;
Location = args?.Location ?? string.Empty;
ApplyDateRange(startDate, endDate, isAllDay);
SelectedCalendar = ResolveSelectedCalendar(args?.SelectedCalendarId);
if (SelectedCalendar != null)
{
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == SelectedCalendar.DefaultShowAs)
?? SelectedShowAsOption
?? ShowAsOptions.FirstOrDefault();
}
}
private AccountCalendarViewModel ResolveSelectedCalendar(Guid? selectedCalendarId)
{
if (selectedCalendarId.HasValue)
{
var selectedCalendar = AvailableCalendars.FirstOrDefault(calendar => calendar.Id == selectedCalendarId.Value);
if (selectedCalendar != null)
return selectedCalendar;
}
return AvailableCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AvailableCalendars.FirstOrDefault();
}
private void ApplyDateRange(DateTime startDate, DateTime endDate, bool isAllDay)
{
IsAllDay = isAllDay;
StartDate = new DateTimeOffset(startDate.Date);
StartTime = startDate.TimeOfDay;
EndTime = endDate.TimeOfDay;
AllDayEndDate = new DateTimeOffset((isAllDay ? endDate.Date : startDate.Date.AddDays(1)));
}
private async Task<CalendarEventComposeResult> BuildResultAsync(List<CalendarComposeAttendeeViewModel> uniqueAttendees)
{
if (RecurrenceEndDate.HasValue && RecurrenceEndDate.Value.Date < StartDate.Date)
{
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidRecurrenceEnd);
}
var htmlNotes = GetHtmlNotesAsync == null ? string.Empty : await GetHtmlNotesAsync();
var effectiveStart = GetEffectiveStartDateTime();
var effectiveEnd = GetEffectiveEndDateTime();
return new CalendarEventComposeResult
{
CalendarId = SelectedCalendar?.Id ?? Guid.Empty,
AccountId = SelectedCalendar?.Account.Id ?? Guid.Empty,
Title = Title.Trim(),
Location = Location?.Trim() ?? string.Empty,
HtmlNotes = htmlNotes,
StartDate = effectiveStart,
EndDate = effectiveEnd,
IsAllDay = IsAllDay,
TimeZoneId = TimeZoneInfo.Local.Id,
ShowAs = SelectedShowAsOption?.ShowAs ?? SelectedCalendar?.DefaultShowAs ?? CalendarItemShowAs.Busy,
SelectedReminders = BuildSelectedReminders(),
Attendees = BuildAttendees(uniqueAttendees),
Attachments = CanAddAttachments
? Attachments.Select(attachment => attachment.ToDraftModel()).ToList()
: [],
Recurrence = BuildRecurrenceRule(),
RecurrenceSummary = RecurrenceSummary
};
}
private List<Reminder> BuildSelectedReminders()
{
if (SelectedReminderOption == null)
return [];
return
[
new Reminder
{
Id = Guid.NewGuid(),
CalendarItemId = Guid.Empty,
DurationInSeconds = SelectedReminderOption.Minutes * 60L,
ReminderType = CalendarItemReminderType.Popup
}
];
}
private static List<CalendarEventAttendee> BuildAttendees(IEnumerable<CalendarComposeAttendeeViewModel> attendees)
{
return attendees
.Select(attendee => new CalendarEventAttendee
{
Id = Guid.NewGuid(),
CalendarItemId = Guid.Empty,
Name = attendee.HasDistinctDisplayName ? attendee.DisplayName : string.Empty,
Email = attendee.Email,
AttendenceStatus = AttendeeStatus.NeedsAction,
IsOrganizer = false,
ResolvedContact = attendee.ResolvedContact
})
.ToList();
}
private ReminderOption GetDefaultReminderOption()
{
var reminderMinutes = Math.Max(1, _preferencesService.DefaultReminderDurationInSeconds / 60);
return ReminderOptions.FirstOrDefault(option => option.Minutes == reminderMinutes)
?? ReminderOptions.FirstOrDefault();
}
private void UpdateRecurrenceSummary()
{
if (!HasInitializedComposeDateRange())
{
RecurrenceSummary = string.Empty;
return;
}
var effectiveStart = GetEffectiveStartDateTime();
var effectiveEnd = GetEffectiveEndDateTime();
var selectedDays = IsDailyRecurrenceSelected
? WeekdayOptions
.Where(option => option.IsSelected)
.Select(option => option.DayOfWeek)
.ToList()
: [];
RecurrenceSummary = CalendarRecurrenceSummaryFormatter.BuildSummary(
IsRecurring,
effectiveStart,
effectiveEnd,
IsAllDay,
CurrentSettings,
SelectedRecurrenceInterval,
SelectedRecurrenceFrequencyOption?.Frequency ?? CalendarItemRecurrenceFrequency.Weekly,
selectedDays,
RecurrenceEndDate);
}
private bool HasInitializedComposeDateRange()
{
if (StartDate == default)
{
return false;
}
return !IsAllDay || AllDayEndDate != default;
}
private string BuildRecurrenceRule()
{
if (!IsRecurring || SelectedRecurrenceFrequencyOption == null)
return string.Empty;
var parts = new List<string>
{
$"FREQ={SelectedRecurrenceFrequencyOption.Frequency.ToString().ToUpperInvariant()}",
$"INTERVAL={SelectedRecurrenceInterval}"
};
var selectedDays = IsDailyRecurrenceSelected
? WeekdayOptions
.Where(option => option.IsSelected)
.Select(option => option.RuleValue)
.ToList()
: [];
if (selectedDays.Count > 0)
{
parts.Add($"BYDAY={string.Join(",", selectedDays)}");
}
if (RecurrenceEndDate.HasValue)
{
var untilValue = IsAllDay
? RecurrenceEndDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture)
: RecurrenceEndDate.Value.Date.AddDays(1).AddSeconds(-1).ToString("yyyyMMdd'T'HHmmss", CultureInfo.InvariantCulture);
parts.Add($"UNTIL={untilValue}");
}
return $"RRULE:{string.Join(";", parts)}";
}
private DateTime GetEffectiveStartDateTime()
=> StartDate.Date.Add(IsAllDay ? TimeSpan.Zero : StartTime);
private DateTime GetEffectiveEndDateTime()
=> IsAllDay
? AllDayEndDate.Date
: StartDate.Date.Add(EndTime);
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
{
var localNow = DateTime.Now;
var roundedMinutes = localNow.Minute switch
{
< 30 => 30,
30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
_ => 60
};
var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
return (startDate, startDate.AddMinutes(30));
}
private CalendarComposeWeekdayOption CreateWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
{
var option = new CalendarComposeWeekdayOption(dayOfWeek, ruleValue, label);
option.PropertyChanged += WeekdayOptionPropertyChanged;
return option;
}
private void WeekdayOptionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CalendarComposeWeekdayOption.IsSelected))
{
UpdateRecurrenceSummary();
}
}
private void SelectSingleWeekday(DayOfWeek dayOfWeek)
{
foreach (var option in WeekdayOptions)
{
option.IsSelected = option.DayOfWeek == dayOfWeek;
}
}
private void ShowValidationMessage(string message)
{
_dialogService.InfoBarMessage(
Translator.CalendarEventCompose_ValidationTitle,
message,
InfoBarMessageType.Warning);
}
private void AttachmentsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasAttachments));
}
private bool TryAddAttachment(string fileName, string filePath, string fileExtension, long size)
{
if (!CanAddAttachments ||
string.IsNullOrWhiteSpace(filePath) ||
Attachments.Any(existing => existing.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
Attachments.Add(new CalendarComposeAttachmentViewModel(fileName, filePath, fileExtension, size));
return true;
}
}
public partial class CalendarComposeFrequencyOption : ObservableObject
{
public CalendarItemRecurrenceFrequency Frequency { get; }
public string DisplayText { get; }
public CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency frequency, string displayText)
{
Frequency = frequency;
DisplayText = displayText;
}
public string PluralLabel(int interval)
{
if (interval == 1)
return DisplayText;
return Frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
_ => DisplayText
};
}
}
public partial class CalendarComposeWeekdayOption : ObservableObject
{
public DayOfWeek DayOfWeek { get; }
public string RuleValue { get; }
public string Label { get; }
public string FullDayName => DayOfWeek switch
{
DayOfWeek.Monday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[1],
DayOfWeek.Tuesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[2],
DayOfWeek.Wednesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[3],
DayOfWeek.Thursday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[4],
DayOfWeek.Friday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[5],
DayOfWeek.Saturday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[6],
DayOfWeek.Sunday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[0],
_ => string.Empty
};
[ObservableProperty]
public partial bool IsSelected { get; set; }
public CalendarComposeWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
{
DayOfWeek = dayOfWeek;
RuleValue = ruleValue;
Label = label;
}
}
@@ -0,0 +1,44 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels;
public partial class CalendarNotificationSettingsPageViewModel : CalendarSettingsSectionViewModelBase
{
[ObservableProperty]
public partial int SelectedDefaultReminderIndex { get; set; }
[ObservableProperty]
public partial int SelectedDefaultSnoozeIndex { get; set; }
public CalendarNotificationSettingsPageViewModel(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
: base(preferencesService, calendarService, accountService)
{
LoadReminderOptions();
LoadSnoozeOptions();
SelectedDefaultReminderIndex = GetSelectedReminderIndex();
SelectedDefaultSnoozeIndex = GetSelectedSnoozeIndex();
IsLoaded = true;
}
partial void OnSelectedDefaultReminderIndexChanged(int value)
{
if (!IsLoaded)
return;
SaveReminderIndex(value);
}
partial void OnSelectedDefaultSnoozeIndexChanged(int value)
{
if (!IsLoaded)
return;
SaveSnoozeIndex(value);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels;
public partial class CalendarPreferenceSettingsPageViewModel : CalendarSettingsSectionViewModelBase
{
[ObservableProperty]
public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; }
[ObservableProperty]
public partial AccountCalendarViewModel SelectedNewEventCalendar { get; set; }
public bool ShouldShowSpecificNewEventCalendar
=> SelectedNewEventBehaviorOption?.Behavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar;
public CalendarPreferenceSettingsPageViewModel(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
: base(preferencesService, calendarService, accountService)
{
LoadNewEventBehaviorOptions();
SelectedNewEventBehaviorOption = GetSelectedNewEventBehaviorOption();
IsLoaded = true;
LoadCalendarsAsync(ApplyStoredNewEventCalendarPreference);
}
partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value)
{
if (!IsLoaded)
return;
OnPropertyChanged(nameof(ShouldShowSpecificNewEventCalendar));
SaveNewEventBehavior(SelectedNewEventBehaviorOption, SelectedNewEventCalendar);
}
partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value)
{
if (!IsLoaded)
return;
SaveNewEventBehavior(SelectedNewEventBehaviorOption, value);
}
private void ApplyStoredNewEventCalendarPreference()
{
var configuredCalendar = ResolveSelectedNewEventCalendar();
if (PreferencesService.NewEventButtonBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && configuredCalendar == null)
{
SelectedNewEventBehaviorOption = NewEventBehaviorOptions.First(option => option.Behavior == NewEventButtonBehavior.AskEachTime);
SelectedNewEventCalendar = null;
return;
}
SelectedNewEventCalendar = configuredCalendar ?? ResolveFallbackNewEventCalendar();
}
}
@@ -0,0 +1,191 @@
using System;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels;
public partial class CalendarRenderingSettingsPageViewModel : CalendarSettingsSectionViewModelBase
{
[ObservableProperty]
public partial double CellHourHeight { get; set; }
[ObservableProperty]
public partial int SelectedFirstDayOfWeekIndex { get; set; }
[ObservableProperty]
public partial bool Is24HourHeaders { get; set; }
[ObservableProperty]
public partial bool IsWorkingHoursEnabled { get; set; }
[ObservableProperty]
public partial TimeSpan WorkingHourStart { get; set; }
[ObservableProperty]
public partial TimeSpan WorkingHourEnd { get; set; }
[ObservableProperty]
public partial int WorkingDayStartIndex { get; set; }
[ObservableProperty]
public partial int WorkingDayEndIndex { get; set; }
[ObservableProperty]
public partial string TimedDayHeaderDateFormat { get; set; } = "ddd dd";
[ObservableProperty]
public partial int SelectedTimedDayHeaderFormatPresetIndex { get; set; } = -1;
public CalendarRenderingSettingsPageViewModel(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
: base(preferencesService, calendarService, accountService)
{
SelectedFirstDayOfWeekIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek));
Is24HourHeaders = preferencesService.Prefer24HourTimeFormat;
IsWorkingHoursEnabled = preferencesService.IsWorkingHoursEnabled;
WorkingHourStart = preferencesService.WorkingHourStart;
WorkingHourEnd = preferencesService.WorkingHourEnd;
CellHourHeight = preferencesService.HourHeight;
WorkingDayStartIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
WorkingDayEndIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
TimedDayHeaderDateFormat = preferencesService.CalendarTimedDayHeaderDateFormat;
SelectedTimedDayHeaderFormatPresetIndex = TimedDayHeaderFormatPresets.IndexOf(TimedDayHeaderDateFormat);
IsLoaded = true;
}
partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
partial void OnIs24HourHeadersChanged(bool value)
{
OnPropertyChanged(nameof(TimedHourLabelPreview));
SaveSettings();
}
partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
partial void OnIsWorkingHoursEnabledChanged(bool 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 OnTimedDayHeaderDateFormatChanged(string value)
{
OnPropertyChanged(nameof(TimedDayHeaderFormatPreview));
OnPropertyChanged(nameof(TimedHourLabelPreview));
var normalizedFormat = string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim();
var matchingPresetIndex = TimedDayHeaderFormatPresets
.Select((format, index) => new { format, index })
.Where(item => string.Equals(item.format, normalizedFormat, StringComparison.Ordinal))
.Select(item => item.index)
.DefaultIfEmpty(-1)
.First();
if (SelectedTimedDayHeaderFormatPresetIndex != matchingPresetIndex)
{
SelectedTimedDayHeaderFormatPresetIndex = matchingPresetIndex;
}
SaveSettings();
}
partial void OnSelectedTimedDayHeaderFormatPresetIndexChanged(int value)
{
if (value < 0 || value >= TimedDayHeaderFormatPresets.Count)
return;
var selectedPreset = TimedDayHeaderFormatPresets[value];
if (string.Equals(TimedDayHeaderDateFormat, selectedPreset, StringComparison.Ordinal))
return;
TimedDayHeaderDateFormat = selectedPreset;
}
public string TimedDayHeaderFormatPreview
{
get
{
var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat.Trim();
var previewDates = new[]
{
new DateTime(2026, 3, 23),
new DateTime(2026, 3, 24),
new DateTime(2026, 3, 25)
};
try
{
return string.Join(" · ", previewDates.Select(date => date.ToString(format, CalendarCulture)));
}
catch (FormatException)
{
return string.Join(" · ", previewDates.Select(date => date.ToString("ddd dd", CalendarCulture)));
}
}
}
public string TimedHourLabelPreview
=> string.Join(" · ", new[] { 0, 9, 14, 24 }.Select(CurrentSettingsPreviewLabel));
private string CurrentSettingsPreviewLabel(int hour)
{
if (Is24HourHeaders)
return hour.ToString(CalendarCulture);
var displayHour = hour % 24;
return DateTime.Today.AddHours(displayHour).ToString("h tt", CalendarCulture);
}
private 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.IsWorkingHoursEnabled = IsWorkingHoursEnabled;
PreferencesService.WorkingHourStart = WorkingHourStart;
PreferencesService.WorkingHourEnd = WorkingHourEnd;
PreferencesService.HourHeight = CellHourHeight;
PreferencesService.CalendarTimedDayHeaderDateFormat = TimedDayHeaderDateFormat;
}
}
@@ -1,127 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Translations;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels
{
public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
{
[ObservableProperty]
private double _cellHourHeight;
[ObservableProperty]
private int _selectedFirstDayOfWeekIndex;
[ObservableProperty]
private bool _is24HourHeaders;
[ObservableProperty]
private TimeSpan _workingHourStart;
[ObservableProperty]
private TimeSpan _workingHourEnd;
[ObservableProperty]
private List<string> _dayNames = [];
[ObservableProperty]
private int _workingDayStartIndex;
[ObservableProperty]
private int _workingDayEndIndex;
public IPreferencesService PreferencesService { get; }
private readonly bool _isLoaded = false;
public CalendarSettingsPageViewModel(IPreferencesService preferencesService)
{
PreferencesService = preferencesService;
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));
_isLoaded = true;
}
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();
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;
Messenger.Send(new CalendarSettingsUpdatedMessage());
}
}
}
@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Translations;
using Wino.Core.ViewModels;
namespace Wino.Calendar.ViewModels;
public abstract class CalendarSettingsSectionViewModelBase : CalendarBaseViewModel
{
protected CalendarSettingsSectionViewModelBase(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
{
PreferencesService = preferencesService;
CalendarService = calendarService;
AccountService = accountService;
var languageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
CalendarCulture = new CultureInfo(languageCode);
for (var index = 0; index < 7; index++)
{
DayNames.Add(CalendarCulture.DateTimeFormat.DayNames[index]);
}
}
protected IPreferencesService PreferencesService { get; }
protected ICalendarService CalendarService { get; }
protected IAccountService AccountService { get; }
protected CultureInfo CalendarCulture { get; }
protected bool IsLoaded { get; set; }
public ObservableCollection<string> DayNames { get; } = [];
public ObservableCollection<string> ReminderOptions { get; } = [];
public ObservableCollection<string> SnoozeOptions { get; } = [];
public ObservableCollection<CalendarNewEventBehaviorOption> NewEventBehaviorOptions { get; } = [];
public ObservableCollection<AccountCalendarViewModel> AvailableNewEventCalendars { get; } = [];
public ObservableCollection<string> TimedDayHeaderFormatPresets { get; } =
[
"ddd dd",
"dddd dd",
"ddd d MMM",
"dd MMM ddd",
"M/d ddd"
];
protected void LoadReminderOptions()
{
ReminderOptions.Clear();
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);
}
}
protected int GetSelectedReminderIndex()
{
if (PreferencesService.DefaultReminderDurationInSeconds == 0)
return 0;
var minutes = (int)(PreferencesService.DefaultReminderDurationInSeconds / 60);
var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes();
var index = Array.IndexOf(predefinedMinutes, minutes);
return index >= 0 ? index + 1 : 0;
}
protected void SaveReminderIndex(int selectedDefaultReminderIndex)
{
if (selectedDefaultReminderIndex == 0)
{
PreferencesService.DefaultReminderDurationInSeconds = 0;
return;
}
var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes();
var minutes = predefinedMinutes[selectedDefaultReminderIndex - 1];
PreferencesService.DefaultReminderDurationInSeconds = minutes * 60;
}
protected void LoadSnoozeOptions()
{
SnoozeOptions.Clear();
foreach (var snoozeMinutes in CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes())
{
SnoozeOptions.Add(string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes));
}
}
protected int GetSelectedSnoozeIndex()
{
var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes().ToArray();
var selectedIndex = Array.IndexOf(supportedSnoozeMinutes, PreferencesService.DefaultSnoozeDurationInMinutes);
return selectedIndex >= 0 ? selectedIndex : 0;
}
protected void SaveSnoozeIndex(int selectedDefaultSnoozeIndex)
{
var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes();
if (supportedSnoozeMinutes.Count == 0)
return;
var selectedIndex = Math.Clamp(selectedDefaultSnoozeIndex, 0, supportedSnoozeMinutes.Count - 1);
PreferencesService.DefaultSnoozeDurationInMinutes = supportedSnoozeMinutes[selectedIndex];
}
protected void LoadNewEventBehaviorOptions()
{
NewEventBehaviorOptions.Clear();
NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AskEachTime, Translator.CalendarSettings_NewEventBehavior_AskEachTime));
NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AlwaysUseSpecificCalendar, Translator.CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar));
}
protected CalendarNewEventBehaviorOption GetSelectedNewEventBehaviorOption()
=> NewEventBehaviorOptions.FirstOrDefault(option => option.Behavior == PreferencesService.NewEventButtonBehavior)
?? NewEventBehaviorOptions.First();
protected async void LoadCalendarsAsync(Action applySelection)
{
var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
var calendarsByAccount = new List<AccountCalendarViewModel>();
foreach (var account in accounts)
{
var calendars = await CalendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
calendarsByAccount.AddRange(calendars.Select(calendar => new AccountCalendarViewModel(account, calendar)));
}
await ExecuteUIThread(() =>
{
AvailableNewEventCalendars.Clear();
foreach (var calendar in calendarsByAccount)
{
AvailableNewEventCalendars.Add(calendar);
}
applySelection();
});
}
protected AccountCalendarViewModel ResolveSelectedNewEventCalendar()
{
var configuredCalendarId = PreferencesService.DefaultNewEventCalendarId;
return configuredCalendarId.HasValue
? AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.Id == configuredCalendarId.Value)
: null;
}
protected AccountCalendarViewModel ResolveFallbackNewEventCalendar()
=> AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.IsPrimary)
?? AvailableNewEventCalendars.FirstOrDefault();
protected void SaveNewEventBehavior(CalendarNewEventBehaviorOption selectedBehaviorOption, AccountCalendarViewModel selectedCalendar)
{
var newEventBehavior = selectedBehaviorOption?.Behavior ?? NewEventButtonBehavior.AskEachTime;
if (newEventBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && selectedCalendar != null)
{
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AlwaysUseSpecificCalendar;
PreferencesService.DefaultNewEventCalendarId = selectedCalendar.Id;
return;
}
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
PreferencesService.DefaultNewEventCalendarId = null;
}
}
public sealed class CalendarNewEventBehaviorOption
{
public CalendarNewEventBehaviorOption(NewEventButtonBehavior behavior, string displayText)
{
Behavior = behavior;
DisplayText = displayText;
}
public NewEventButtonBehavior Behavior { get; }
public string DisplayText { get; }
}
@@ -1,13 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Wino.Core;
namespace Wino.Calendar.ViewModels
{
public static class CalendarViewModelContainerSetup
{
public static void RegisterCalendarViewModelServices(this IServiceCollection services)
{
services.RegisterCoreServices();
}
}
}
@@ -2,69 +2,92 @@
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
namespace Wino.Calendar.ViewModels.Data;
public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar
{
public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar
public MailAccount Account { get; }
public AccountCalendar AccountCalendar { get; }
public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar)
{
public MailAccount Account { get; }
public AccountCalendar AccountCalendar { get; }
Account = account;
AccountCalendar = accountCalendar;
public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar)
{
Account = account;
AccountCalendar = accountCalendar;
IsChecked = accountCalendar.IsExtended;
}
IsChecked = accountCalendar.IsExtended;
}
[ObservableProperty]
private bool _isChecked;
[ObservableProperty]
private bool _isChecked;
partial void OnIsCheckedChanged(bool value) => IsExtended = value;
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 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 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 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 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 IsPrimary
{
get => AccountCalendar.IsPrimary;
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
}
public bool IsReadOnly
{
get => AccountCalendar.IsReadOnly;
set => SetProperty(AccountCalendar.IsReadOnly, value, AccountCalendar, (u, i) => u.IsReadOnly = i);
}
public Guid AccountId
{
get => AccountCalendar.AccountId;
set => SetProperty(AccountCalendar.AccountId, value, AccountCalendar, (u, a) => u.AccountId = a);
}
public bool IsSynchronizationEnabled
{
get => AccountCalendar.IsSynchronizationEnabled;
set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i);
}
public string RemoteCalendarId
{
get => AccountCalendar.RemoteCalendarId;
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
}
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
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 => AccountCalendar.MailAccount ?? Account;
set => AccountCalendar.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,57 @@
using System;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Extensions;
namespace Wino.Calendar.ViewModels.Data;
public class CalendarComposeAttachmentViewModel
{
public Guid Id { get; } = Guid.NewGuid();
public string FileName { get; }
public string FilePath { get; }
public string FileExtension { get; }
public long Size { get; }
public string ReadableSize => Size.GetBytesReadable();
public MailAttachmentType AttachmentType { get; }
public CalendarComposeAttachmentViewModel(string fileName, string filePath, string fileExtension, long size)
{
FileName = fileName;
FilePath = filePath;
FileExtension = fileExtension;
Size = size;
AttachmentType = GetAttachmentType(fileExtension);
}
public CalendarEventComposeAttachmentDraft ToDraftModel()
{
return new CalendarEventComposeAttachmentDraft
{
Id = Id,
FileName = FileName,
FilePath = FilePath,
FileExtension = FileExtension,
Size = Size
};
}
private static MailAttachmentType GetAttachmentType(string extension)
{
if (string.IsNullOrWhiteSpace(extension))
return MailAttachmentType.None;
return extension.ToLowerInvariant() switch
{
".exe" => MailAttachmentType.Executable,
".rar" => MailAttachmentType.RarArchive,
".zip" => MailAttachmentType.Archive,
".ogg" or ".mp3" or ".wav" or ".aac" or ".alac" => MailAttachmentType.Audio,
".mp4" or ".wmv" or ".avi" or ".flv" => MailAttachmentType.Video,
".pdf" => MailAttachmentType.PDF,
".htm" or ".html" => MailAttachmentType.HTML,
".png" or ".jpg" or ".jpeg" or ".gif" or ".jiff" => MailAttachmentType.Image,
_ => MailAttachmentType.Other
};
}
}
@@ -0,0 +1,23 @@
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Calendar.ViewModels.Data;
public class CalendarComposeAttendeeViewModel : IContactDisplayItem
{
public string DisplayName { get; }
public string Email { get; }
public AccountContact ResolvedContact { get; }
public string Address => Email;
public AccountContact PreviewContact => ResolvedContact;
public bool HasDistinctDisplayName => !string.IsNullOrWhiteSpace(DisplayName) && !DisplayName.Equals(Email, System.StringComparison.OrdinalIgnoreCase);
public CalendarComposeAttendeeViewModel(string displayName, string email, AccountContact resolvedContact = null)
{
DisplayName = string.IsNullOrWhiteSpace(displayName) ? email : displayName;
Email = email;
ResolvedContact = resolvedContact;
}
public static CalendarComposeAttendeeViewModel FromContact(AccountContact contact)
=> new(contact.Name, contact.Address, contact);
}
@@ -2,45 +2,197 @@
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
namespace Wino.Calendar.ViewModels.Data;
public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, ICalendarItemViewModel
{
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
{
public CalendarItem CalendarItem { get; }
public string Title => CalendarItem.Title;
public Guid Id => CalendarItem.Id;
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; }
public DateTime EndDate => CalendarItem.EndDate;
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
public ITimePeriod Period => CalendarItem.Period;
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]
private bool _isSelected;
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
public CalendarItemViewModel(CalendarItem calendarItem)
get
{
CalendarItem = calendarItem;
// Get start date in user's local timezone
return CalendarItem.LocalStartDate;
}
set
{
// All-day events use floating dates and should not shift across timezones.
CalendarItem.StartDate = CalendarItem.IsAllDayEvent
? value.Date
: value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
}
public override string ToString() => CalendarItem.Title;
}
/// <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;
public bool CanDragDrop => CalendarItem.CanChangeStartAndEndDate;
[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(CanDragDrop));
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;
}
@@ -1,146 +1,202 @@
using System;
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;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Calendar.ViewModels.Data
namespace Wino.Calendar.ViewModels.Data;
public partial class GroupedAccountCalendarViewModel : ObservableObject
{
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)
{
public event EventHandler CollectiveSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel> CalendarSelectionStateChanged;
Account = account;
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
AccountColorHex = account.AccountColorHex;
public MailAccount Account { get; }
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
ManageIsCheckedState();
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
foreach (var calendarViewModel in calendarViewModels)
{
Account = account;
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
ManageIsCheckedState();
foreach (var calendarViewModel in calendarViewModels)
{
calendarViewModel.PropertyChanged += CalendarPropertyChanged;
}
AccountCalendars.CollectionChanged += CalendarListUpdated;
calendarViewModel.PropertyChanged += CalendarPropertyChanged;
}
private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
AccountCalendars.CollectionChanged += CalendarListUpdated;
}
private void CalendarListUpdated(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
if (e.Action == NotifyCollectionChangedAction.Add)
foreach (AccountCalendarViewModel calendar in e.NewItems)
{
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;
}
calendar.PropertyChanged += CalendarPropertyChanged;
}
}
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (sender is AccountCalendarViewModel viewModel)
foreach (AccountCalendarViewModel calendar in e.OldItems)
{
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
{
ManageIsCheckedState();
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
}
calendar.PropertyChanged -= CalendarPropertyChanged;
}
}
[ObservableProperty]
private bool _isExpanded = true;
[ObservableProperty]
private bool? isCheckedState = true;
private bool _isExternalPropChangeBlocked = false;
private void ManageIsCheckedState()
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
if (_isExternalPropChangeBlocked) return;
_isExternalPropChangeBlocked = true;
if (AccountCalendars.All(c => c.IsChecked))
foreach (AccountCalendarViewModel calendar in e.OldItems)
{
IsCheckedState = true;
calendar.PropertyChanged -= CalendarPropertyChanged;
}
else if (AccountCalendars.All(c => !c.IsChecked))
{
IsCheckedState = false;
}
else
{
IsCheckedState = null;
}
_isExternalPropChangeBlocked = false;
}
partial void OnIsCheckedStateChanged(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);
}
}
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is AccountCalendarViewModel viewModel &&
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;
[ObservableProperty]
public partial string AccountColorHex { get; set; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronize), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
public partial bool IsSynchronizationInProgress { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int TotalItemsToSync { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; }
[ObservableProperty]
public partial string SynchronizationStatus { get; set; } = string.Empty;
public bool CanSynchronize => !IsSynchronizationInProgress;
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
public double SynchronizationProgress
{
get
{
if (TotalItemsToSync <= 0)
return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
}
}
public double SynchronizationProgressValue => SynchronizationProgress;
private bool _isExternalPropChangeBlocked;
public void ApplySynchronizationProgress(AccountSynchronizationProgress progress)
{
if (progress == null || progress.AccountId != Account.Id)
return;
IsSynchronizationInProgress = progress.IsInProgress;
TotalItemsToSync = progress.TotalUnits;
RemainingItemsToSync = progress.RemainingUnits;
SynchronizationStatus = progress.Status ?? string.Empty;
}
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;
_isExternalPropChangeBlocked = true;
if (newValue == null)
{
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;
if (_isExternalPropChangeBlocked)
return;
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
}
public void UpdateAccount(MailAccount updatedAccount)
{
if (updatedAccount == null || updatedAccount.Id != Account.Id)
return;
Account.Name = updatedAccount.Name;
Account.Address = updatedAccount.Address;
Account.AccountColorHex = updatedAccount.AccountColorHex;
Account.AttentionReason = updatedAccount.AttentionReason;
Account.MergedInboxId = updatedAccount.MergedInboxId;
AccountColorHex = updatedAccount.AccountColorHex;
OnPropertyChanged(nameof(Account));
}
}
@@ -1,116 +1,884 @@
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.Extensions;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Services;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels
namespace Wino.Calendar.ViewModels;
public partial class EventDetailsPageViewModel : CalendarBaseViewModel
{
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;
private readonly INotificationBuilder _notificationBuilder;
private readonly IContactService _contactService;
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)
{
private readonly ICalendarService _calendarService;
private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
// Notify the view to re-render the description
Messenger.Send(new CalendarDescriptionRenderingRequested());
}
public CalendarSettings CurrentSettings { get; }
[ObservableProperty]
public partial CalendarItemViewModel SeriesParent { get; set; }
[ObservableProperty]
public partial List<Reminder> Reminders { get; set; }
#region Details
public ObservableCollection<ReminderOption> ReminderOptions { get; } = new ObservableCollection<ReminderOption>();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
private CalendarItemViewModel _currentEvent;
/// <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;
[ObservableProperty]
private CalendarItemViewModel _seriesParent;
/// <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;
public bool CanViewSeries => 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
#endregion
public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService)
#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
{
_calendarService = calendarService;
_nativeAppService = nativeAppService;
_preferencesService = preferencesService;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
Messenger.Send(new DetailsPageStateChangedMessage(true));
if (parameters == null || parameters is not CalendarItemTarget args)
return;
await LoadCalendarItemTargetAsync(args);
}
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
{
try
{
var currentEventItem = await _calendarService.GetCalendarItemTargetAsync(target);
if (currentEventItem == null)
return;
CurrentEvent = new CalendarItemViewModel(currentEventItem);
var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.EventTrackingId);
foreach (var item in attendees)
{
CurrentEvent.Attendees.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
base.OnNavigatedFrom(mode, parameters);
Messenger.Send(new DetailsPageStateChangedMessage(false));
}
[RelayCommand]
private async Task SaveAsync()
{
}
[RelayCommand]
private async Task DeleteAsync()
{
}
[RelayCommand]
private Task JoinOnline()
{
if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) return Task.CompletedTask;
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
}
[RelayCommand]
private async Task Respond(CalendarItemStatus status)
{
if (CurrentEvent == null) return;
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,
INotificationBuilder notificationBuilder,
IUnderlyingThemeService underlyingThemeService,
IContactService contactService)
{
_calendarService = calendarService;
_nativeAppService = nativeAppService;
_preferencesService = preferencesService;
_dialogService = dialogService;
_winoRequestDelegator = winoRequestDelegator;
_navigationService = navigationService;
_underlyingThemeService = underlyingThemeService;
_notificationBuilder = notificationBuilder;
_contactService = contactService;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
foreach (var showAs in CalendarItemActionOptions.ShowAsOptions)
{
ShowAsOptions.Add(new ShowAsOption(showAs));
}
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy) ?? ShowAsOptions.FirstOrDefault();
foreach (var responseStatus in CalendarItemActionOptions.ResponseOptions)
{
RsvpStatusOptions.Add(new RsvpStatusOption(responseStatus));
}
}
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, EntityUpdateSource source)
{
base.OnCalendarItemUpdated(calendarItem, source);
// If the current event was updated, reload it
if (IsCurrentEventMatch(calendarItem))
{
// Reflect client-side optimistic changes immediately; fallback to DB for server updates.
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
{
var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
CurrentEvent = new CalendarItemViewModel(calendarItem)
{
IsBusy = source == EntityUpdateSource.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 async void OnCalendarItemAdded(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemAdded(calendarItem, source);
if (!IsCurrentEventMatch(calendarItem))
return;
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
{
CurrentEvent = new CalendarItemViewModel(calendarItem)
{
IsBusy = source == EntityUpdateSource.ClientUpdated
};
return;
}
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, EntityUpdateSource source)
{
base.OnCalendarItemDeleted(calendarItem, source);
// If the current event was deleted, navigate back
if (IsCurrentEventMatch(calendarItem))
{
NavigateBackToCalendar(forceReload: true);
}
}
private bool IsCurrentEventMatch(CalendarItem calendarItem)
{
if (CurrentEvent?.CalendarItem == null || calendarItem == null)
return false;
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
return CurrentEvent.CalendarItem.Id == calendarItem.Id ||
(trackedLocalItemId.HasValue && CurrentEvent.CalendarItem.Id == trackedLocalItemId.Value) ||
CurrentEvent.CalendarItem.RecurringCalendarItemId == calendarItem.Id;
}
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)
{
var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
// Resolve contacts for all attendees in a single batch DB query.
var emails = attendees
.Where(a => !string.IsNullOrEmpty(a.Email))
.Select(a => a.Email)
.ToList();
if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
emails.Add(calendarItem.OrganizerEmail);
var contacts = await _contactService.GetContactsByAddressesAsync(emails).ConfigureAwait(false);
var contactLookup = contacts.ToDictionary(c => c.Address, StringComparer.OrdinalIgnoreCase);
foreach (var attendee in attendees)
{
if (!string.IsNullOrEmpty(attendee.Email) && contactLookup.TryGetValue(attendee.Email, out var contact))
attendee.ResolvedContact = contact;
}
// 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();
var attendeesForUi = new List<CalendarEventAttendee>();
// If the organizer is in the list, add them first
if (organizer != null)
{
attendeesForUi.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
};
if (contactLookup.TryGetValue(calendarItem.OrganizerEmail, out var organizerContact))
organizerAttendee.ResolvedContact = organizerContact;
attendeesForUi.Add(organizerAttendee);
}
// Add all other attendees after the organizer
foreach (var item in nonOrganizerAttendees)
{
attendeesForUi.Add(item);
}
await ExecuteUIThread(() =>
{
if (CurrentEvent == null)
return;
CurrentEvent.Attendees.Clear();
foreach (var attendee in attendeesForUi)
{
CurrentEvent.Attendees.Add(attendee);
}
});
}
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;
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
{
_dialogService.ShowReadOnlyCalendarMessage();
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);
NavigateBackToCalendar(forceReload: true);
}
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 (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
{
_dialogService.ShowReadOnlyCalendarMessage();
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);
NavigateBackToCalendar(forceReload: true);
}
catch (Exception ex)
{
Debug.WriteLine($"Error deleting calendar event: {ex.Message}");
}
}
private void NavigateBackToCalendar(bool forceReload)
{
var navigationDate = CurrentEvent?.CalendarItem.LocalStartDate ?? DateTime.Now;
_navigationService.Navigate(
WinoPage.CalendarPage,
new CalendarPageNavigationArgs
{
NavigationDate = navigationDate,
ForceReload = forceReload
});
}
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{
if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete)
return;
await DeleteAsync();
args.Handled = true;
}
[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 Task CreateTestNotificationAsync()
{
if (CurrentEvent?.CalendarItem == null)
return Task.CompletedTask;
var reminderDurationInSeconds = Reminders?
.Where(x => x.DurationInSeconds > 0)
.OrderByDescending(x => x.DurationInSeconds)
.Select(x => x.DurationInSeconds)
.FirstOrDefault() ?? 0;
if (reminderDurationInSeconds <= 0)
reminderDurationInSeconds = Math.Max(_preferencesService.DefaultReminderDurationInSeconds, 30 * 60);
return _notificationBuilder.CreateCalendarReminderNotificationAsync(CurrentEvent.CalendarItem, reminderDurationInSeconds);
}
[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;
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
{
_dialogService.ShowReadOnlyCalendarMessage();
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;
}
}
@@ -2,30 +2,33 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
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
namespace Wino.Calendar.ViewModels.Interfaces;
public interface IAccountCalendarStateService : INotifyPropertyChanged
{
public interface IAccountCalendarStateService : INotifyPropertyChanged
{
ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; }
IDispatcher Dispatcher { get; set; }
ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; }
event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void ClearGroupedAccountCalendar();
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void ClearGroupedAccountCalendars();
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
/// <summary>
/// Enumeration of currently selected calendars.
/// </summary>
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable { get; }
}
/// <summary>
/// Enumeration of currently selected calendars.
/// </summary>
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
bool IsAnySynchronizationInProgress { get; }
ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
}
@@ -0,0 +1,8 @@
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Messages;
public sealed record CalendarItemContextActionRequestedMessage(
CalendarItemViewModel CalendarItemViewModel,
CalendarContextMenuAction Action);
@@ -1,14 +1,13 @@
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages
{
public class CalendarItemDoubleTappedMessage
{
public CalendarItemDoubleTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
namespace Wino.Calendar.ViewModels.Messages;
public CalendarItemViewModel CalendarItemViewModel { get; }
public class CalendarItemDoubleTappedMessage
{
public CalendarItemDoubleTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}
@@ -1,14 +1,13 @@
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages
{
public class CalendarItemRightTappedMessage
{
public CalendarItemRightTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
namespace Wino.Calendar.ViewModels.Messages;
public CalendarItemViewModel CalendarItemViewModel { get; }
public class CalendarItemRightTappedMessage
{
public CalendarItemRightTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}
@@ -1,17 +1,12 @@
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Messages;
namespace Wino.Calendar.ViewModels.Messages
public class CalendarItemTappedMessage
{
public class CalendarItemTappedMessage
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
{
CalendarItemViewModel = calendarItemViewModel;
ClickedPeriod = clickedPeriod;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
public CalendarDayModel ClickedPeriod { get; }
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}
@@ -1,16 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12</LangVersion>
<Platforms>AnyCPU;x64;x86</Platforms>
<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="Microsoft.Identity.Client" Version="4.66.2" />
<PackageReference Include="TimePeriodLibrary.NET" Version="2.1.5" />
<PackageReference Include="TimePeriodLibrary.NET" />
<PackageReference Include="EmailValidation" />
</ItemGroup>
<ItemGroup>
-308
View File
@@ -1,308 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35424.110
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.Domain", "Wino.Core.Domain\Wino.Core.Domain.csproj", "{814400B6-5A05-4596-B451-3A116A147DC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.UWP", "Wino.Core.UWP\Wino.Core.UWP.csproj", "{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.ViewModels", "Wino.Core.ViewModels\Wino.Core.ViewModels.csproj", "{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Messaging", "Wino.Messages\Wino.Messaging.csproj", "{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Server", "Wino.Server\Wino.Server.csproj", "{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core", "Wino.Core\Wino.Core.csproj", "{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar", "Wino.Calendar\Wino.Calendar.csproj", "{600F4979-DB7E-409D-B7DA-B60BE4C55C35}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.SourceGenerators", "Wino.SourceGenerators\Wino.SourceGenerators.csproj", "{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}"
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Wino.Calendar.Packaging", "Wino.Calendar.Packaging\Wino.Calendar.Packaging.wapproj", "{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Calendar.ViewModels", "Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj", "{CF850F8C-5042-4376-9CBA-C8F2BB554083}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Services", "Wino.Services\Wino.Services.csproj", "{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Authentication", "Wino.Authentication\Wino.Authentication.csproj", "{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM = Debug|ARM
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM = Release|ARM
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM64.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x64.ActiveCfg = Debug|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x64.Build.0 = Debug|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x86.ActiveCfg = Debug|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x86.Build.0 = Debug|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|Any CPU.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM64.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM64.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x64.ActiveCfg = Release|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x64.Build.0 = Release|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x86.ActiveCfg = Release|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x86.Build.0 = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM.ActiveCfg = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM.Build.0 = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.Build.0 = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.ActiveCfg = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.Build.0 = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.ActiveCfg = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.Build.0 = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|Any CPU.Build.0 = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM.ActiveCfg = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM.Build.0 = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.ActiveCfg = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.Build.0 = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.ActiveCfg = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.Build.0 = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.ActiveCfg = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.Build.0 = Release|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM64.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x64.ActiveCfg = Debug|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x64.Build.0 = Debug|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x86.ActiveCfg = Debug|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x86.Build.0 = Debug|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|Any CPU.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM64.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM64.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x64.ActiveCfg = Release|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x64.Build.0 = Release|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x86.ActiveCfg = Release|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x86.Build.0 = Release|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM64.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x64.ActiveCfg = Debug|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x64.Build.0 = Debug|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x86.ActiveCfg = Debug|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x86.Build.0 = Debug|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|Any CPU.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM64.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM64.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x64.ActiveCfg = Release|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x64.Build.0 = Release|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x86.ActiveCfg = Release|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x86.Build.0 = Release|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|Any CPU.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|Any CPU.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM64.Build.0 = Debug|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x64.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x64.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x86.ActiveCfg = Debug|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x86.Build.0 = Debug|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|Any CPU.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|Any CPU.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM64.ActiveCfg = Release|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM64.Build.0 = Release|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x64.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x64.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x86.ActiveCfg = Release|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x86.Build.0 = Release|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM64.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x64.ActiveCfg = Debug|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x64.Build.0 = Debug|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x86.ActiveCfg = Debug|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x86.Build.0 = Debug|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|Any CPU.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM64.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM64.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x64.ActiveCfg = Release|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x64.Build.0 = Release|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x86.ActiveCfg = Release|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x86.Build.0 = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.ActiveCfg = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.Build.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.Deploy.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.ActiveCfg = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.Build.0 = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.Deploy.0 = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.ActiveCfg = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Build.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Deploy.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.ActiveCfg = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Build.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Deploy.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.ActiveCfg = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Build.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Deploy.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.ActiveCfg = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.Build.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.Deploy.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.ActiveCfg = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.Build.0 = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.Deploy.0 = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.ActiveCfg = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Build.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Deploy.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.ActiveCfg = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Build.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Deploy.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.ActiveCfg = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Build.0 = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Deploy.0 = Release|x86
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM64.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x64.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x86.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM64.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM64.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x64.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x64.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x86.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x86.Build.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.ActiveCfg = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.Build.0 = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.Deploy.0 = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Build.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Deploy.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.ActiveCfg = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Build.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Deploy.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.ActiveCfg = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Build.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Deploy.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.Build.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.Deploy.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.ActiveCfg = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.Build.0 = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.Deploy.0 = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.ActiveCfg = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Build.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Deploy.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.ActiveCfg = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Build.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Deploy.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.ActiveCfg = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Build.0 = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Deploy.0 = Release|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM64.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x64.ActiveCfg = Debug|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x64.Build.0 = Debug|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x86.ActiveCfg = Debug|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x86.Build.0 = Debug|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|Any CPU.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM64.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM64.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x64.ActiveCfg = Release|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x64.Build.0 = Release|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x86.ActiveCfg = Release|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x86.Build.0 = Release|x86
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM64.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x64.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x64.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x86.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x86.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|Any CPU.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM64.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM64.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x64.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x64.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x86.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x86.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM64.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x64.ActiveCfg = Debug|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x64.Build.0 = Debug|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x86.ActiveCfg = Debug|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x86.Build.0 = Debug|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|Any CPU.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM64.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM64.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x64.ActiveCfg = Release|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x64.Build.0 = Release|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x86.ActiveCfg = Release|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
@@ -1,24 +0,0 @@
using System.Threading.Tasks;
using Windows.ApplicationModel.Activation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;
using Wino.Activation;
using Wino.Calendar.Views;
namespace Wino.Calendar.Activation
{
public class DefaultActivationHandler : ActivationHandler<IActivatedEventArgs>
{
protected override Task HandleInternalAsync(IActivatedEventArgs args)
{
(Window.Current.Content as Frame).Navigate(typeof(AppShell), null, new DrillInNavigationTransitionInfo());
return Task.CompletedTask;
}
// Only navigate if Frame content doesn't exist.
protected override bool CanHandleInternal(IActivatedEventArgs args)
=> (Window.Current?.Content as Frame)?.Content == null;
}
}
-31
View File
@@ -1,31 +0,0 @@
<core:WinoApplication
x:Class="Wino.Calendar.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
xmlns:core="using:Wino.Core.UWP"
xmlns:coreStyles="using:Wino.Core.UWP.Styles"
xmlns:local="using:Wino.Calendar"
xmlns:styles="using:Wino.Calendar.Styles">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<core:CoreGeneric />
<styles:WinoCalendarResources />
<ResourceDictionary Source="Styles/CalendarThemeResources.xaml" />
<ResourceDictionary Source="Styles/WinoDayTimelineCanvas.xaml" />
<ResourceDictionary Source="Styles/WinoCalendarView.xaml" />
<ResourceDictionary Source="Styles/WinoCalendarTypeSelectorControl.xaml" />
<!-- Last item must always be the default theme. -->
<ResourceDictionary Source="ms-appx:///Wino.Core.UWP/AppThemes/Mica.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</core:WinoApplication>
-164
View File
@@ -1,164 +0,0 @@
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.UI.Core.Preview;
using Wino.Activation;
using Wino.Calendar.Activation;
using Wino.Calendar.Services;
using Wino.Calendar.ViewModels;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.UWP;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Connection;
using Wino.Messaging.Server;
using Wino.Services;
namespace Wino.Calendar
{
public sealed partial class App : WinoApplication, IRecipient<NewCalendarSynchronizationRequested>
{
public override string AppCenterKey => "dfdad6ab-95f9-44cc-9112-45ec6730c49e";
private BackgroundTaskDeferral connectionBackgroundTaskDeferral;
private BackgroundTaskDeferral toastActionBackgroundTaskDeferral;
public App()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register(this);
}
public override IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.RegisterSharedServices();
services.RegisterCalendarViewModelServices();
services.RegisterCoreUWPServices();
services.RegisterCoreViewModels();
RegisterUWPServices(services);
RegisterViewModels(services);
RegisterActivationHandlers(services);
return services.BuildServiceProvider();
}
#region Dependency Injection
private void RegisterActivationHandlers(IServiceCollection services)
{
//services.AddTransient<ProtocolActivationHandler>();
//services.AddTransient<ToastNotificationActivationHandler>();
//services.AddTransient<FileActivationHandler>();
}
private void RegisterUWPServices(IServiceCollection services)
{
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<ICalendarDialogService, DialogService>();
services.AddTransient<ISettingsBuilderService, SettingsBuilderService>();
services.AddTransient<IProviderService, ProviderService>();
services.AddSingleton<IAuthenticatorConfig, CalendarAuthenticatorConfig>();
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
}
private void RegisterViewModels(IServiceCollection services)
{
services.AddSingleton(typeof(AppShellViewModel));
services.AddSingleton(typeof(CalendarPageViewModel));
services.AddTransient(typeof(CalendarSettingsPageViewModel));
services.AddTransient(typeof(AccountManagementViewModel));
services.AddTransient(typeof(PersonalizationPageViewModel));
services.AddTransient(typeof(AccountDetailsPageViewModel));
services.AddTransient(typeof(EventDetailsPageViewModel));
}
#endregion
protected override void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
{
// TODO: Check server running.
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
LogActivation($"OnLaunched -> {args.GetType().Name}, Kind -> {args.Kind}, PreviousExecutionState -> {args.PreviousExecutionState}, IsPrelaunch -> {args.PrelaunchActivated}");
if (!args.PrelaunchActivated)
{
await ActivateWinoAsync(args);
}
}
protected override IEnumerable<ActivationHandler> GetActivationHandlers()
{
return null;
}
protected override ActivationHandler<IActivatedEventArgs> GetDefaultActivationHandler()
=> new DefaultActivationHandler();
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails appServiceTriggerDetails)
{
LogActivation("OnBackgroundActivated -> AppServiceTriggerDetails received.");
// Only accept connections from callers in the same package
if (appServiceTriggerDetails.CallerPackageFamilyName == Package.Current.Id.FamilyName)
{
// Connection established from the fulltrust process
connectionBackgroundTaskDeferral = args.TaskInstance.GetDeferral();
args.TaskInstance.Canceled += OnConnectionBackgroundTaskCanceled;
AppServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection;
WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstablished());
}
}
}
public void OnConnectionBackgroundTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
sender.Canceled -= OnConnectionBackgroundTaskCanceled;
Log.Information($"Server connection background task was canceled. Reason: {reason}");
connectionBackgroundTaskDeferral?.Complete();
connectionBackgroundTaskDeferral = null;
AppServiceConnectionManager.Connection = null;
}
public async void Receive(NewCalendarSynchronizationRequested message)
{
try
{
var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(message);
synchronizationResultResponse.ThrowIfFailed();
}
catch (WinoServerException serverException)
{
var dialogService = Services.GetService<ICalendarDialogService>();
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
}
}
}
}
@@ -1,41 +0,0 @@
using System;
using Windows.Foundation;
namespace Wino.Calendar.Args
{
/// <summary>
/// When a new timeline cell is selected.
/// </summary>
public class TimelineCellSelectedArgs : EventArgs
{
public TimelineCellSelectedArgs(DateTime clickedDate, Point canvasPoint, Point positionerPoint, Size cellSize)
{
ClickedDate = clickedDate;
CanvasPoint = canvasPoint;
PositionerPoint = positionerPoint;
CellSize = cellSize;
}
/// <summary>
/// Clicked date and time information for the cell.
/// </summary>
public DateTime ClickedDate { get; set; }
/// <summary>
/// Position relative to the cell drawing part of the canvas.
/// Used to detect clicked cell from the position.
/// </summary>
public Point CanvasPoint { get; }
/// <summary>
/// Position relative to the main root positioner element of the drawing canvas.
/// Used to show the create event dialog teaching tip in correct position.
/// </summary>
public Point PositionerPoint { get; }
/// <summary>
/// Size of the cell.
/// </summary>
public Size CellSize { get; }
}
}
@@ -1,9 +0,0 @@
using System;
namespace Wino.Calendar.Args
{
/// <summary>
/// When selected timeline cell is unselected.
/// </summary>
public class TimelineCellUnselectedArgs : EventArgs { }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More