293 Commits
v1.8.4 ... main

Author SHA1 Message Date
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
Aleh Khantsevich
fcaf62ecf7 View message source (#541)
* Added view message source

* Change condition to when button available
2025-02-01 18:13:36 +01:00
Burak Kaan Köse
7cfa5a57f5 Event details page navigation, handling of attendees in Outlook synchronizer, navigation changes for calendar. 2025-01-16 22:00:05 +01:00
Burak Kaan Köse
56d6c22d53 Event details page basic layout. 2025-01-14 00:53:54 +01:00
Burak Kaan Köse
10b0b1e96e Revert debug code for background synchronizer. 2025-01-09 22:24:45 +01:00
Burak Kaan Köse
6ef1e9c86c Fix print icon. 2025-01-07 22:23:03 +01:00
Burak Kaan Köse
a0d0f2651b Release x64 build properties for Calendar. 2025-01-07 22:13:07 +01:00
Burak Kaan Köse
b6edbad744 Fix multi-day event recurrences for monthly calendar. 2025-01-07 22:12:54 +01:00
Sean Chen
05748e23b1 Fix crash on launch when IMAP accounts are present (#507) 2025-01-07 22:10:37 +01:00
Burak Kaan Köse
12d87be106 Speeding up the UI by changing the flip view orientation at correct moment. 2025-01-07 20:51:10 +01:00
Burak Kaan Köse
dfb83cc1f7 Sync calendar events for all accounts for testing. 2025-01-07 14:38:45 +01:00
Burak Kaan Köse
1840ae80c2 Fix google calendar sync for bg colors. 2025-01-07 14:38:29 +01:00
Burak Kaan Köse
2e9d1d83a4 Outlook delta synchronization. 2025-01-07 13:42:10 +01:00
Burak Kaan Köse
1ef83a3089 Bump version. 2025-01-07 01:14:05 +01:00
Burak Kaan Köse
b95c06a5dc Fix startup launch for Mail. 2025-01-07 01:13:34 +01:00
Burak Kaan Köse
8f789841a6 Fix manage account navigation for Wino Mail 2025-01-07 01:13:25 +01:00
Burak Kaan Köse
0f57a4dfd7 Monthly calendar basics. 2025-01-06 21:56:33 +01:00
Burak Kaan Köse
125c277c88 Outlook calendar/event syncing basics without delta. Bunch of UI updates for the calendar view. 2025-01-06 02:15:21 +01:00
Burak Kaan Köse
a7674d436d Handling of multi-day events, new rendering etc. 2025-01-04 11:39:32 +01:00
Burak Kaan Köse
48ba4cdf42 Fixed an issue where some children does not fit in the arranged box. 2025-01-02 22:24:04 +01:00
Burak Kaan Köse
8a9265eb79 Bunch of interaction updates for the calendar item control. 2025-01-02 00:18:34 +01:00
Burak Kaan Köse
215dc6ea6d Basic dragging state for calendar control 2025-01-01 20:19:31 +01:00
Burak Kaan Köse
428dcb2348 New quick event dialog using Popup. 2025-01-01 19:17:54 +01:00
Burak Kaan Köse
1c79d14260 Finalizing quick event dialog. 2025-01-01 17:28:29 +01:00
Burak Kaan Köse
a82b487b92 Disable auto embedding of videos pasted on the editor. 2025-01-01 10:49:01 +01:00
Burak Kaan Köse
068369fca2 Some improvements on the quick event tip. 2024-12-31 22:22:19 +01:00
Burak Kaan Köse
d524143a53 Some adjustments for all day items control. Removed the leaking part. 2024-12-31 19:21:07 +01:00
Burak Kaan Köse
05ebfa68c9 Idle state loading for calendar. 2024-12-31 15:32:03 +01:00
Burak Kaan Köse
57d8fd7e10 Disable all day items control due to memory leak. 2024-12-31 14:28:37 +01:00
Burak Kaan Köse
64c556a337 Fixing load more issue during flip switches. 2024-12-31 14:28:28 +01:00
Burak Kaan Köse
de268d1168 Fixing on calendar event added, and testing method. 2024-12-31 01:30:10 +01:00
Burak Kaan Köse
8fd09bcad4 Proper handling of DateTimeOffset, support for Multi-Day events and reacting to adding/removing events for the days. 2024-12-30 23:10:51 +01:00
Burak Kaan Köse
8cc7d46d7b Updating visibilities on calendar events on calendar activation/deactivation. 2024-12-30 01:15:31 +01:00
Burak Kaan Köse
bb56815210 Readable coloring for calendar events. 2024-12-30 01:15:06 +01:00
Burak Kaan Köse
332f2ab89f Removed xaml converter for resolving mail subjects. (#497) 2024-12-29 23:00:42 +01:00
Burak Kaan Köse
d3780244cd Improved calendar rendering, standartization of some templates and cleanup of old styles. 2024-12-29 22:30:00 +01:00
Burak Kaan Köse
f7bfbd5080 Encapsulation of grouped account selection events and collective events. 2024-12-29 19:37:36 +01:00
Burak Kaan Köse
eef2ee1baa Listing account calendars on the shell, some visual state updates and reacting to calendar setting changes properly. 2024-12-29 17:41:54 +01:00
Burak Kaan Köse
8d8d7d0f8c Handling of basic all-day events for calendar. 2024-12-29 13:06:35 +01:00
Burak Kaan Köse
979a3d8f1f Displaying events and all-day events. 2024-12-28 23:17:16 +01:00
Burak Kaan Köse
95b8f54b27 Implementing themes for Calendar. 2024-12-28 20:42:03 +01:00
Burak Kaan Köse
5f1d411b28 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-12-28 16:39:58 +01:00
Burak Kaan Köse
6e3fcf363a Calendar page and shell improvements to support navigations. Enabled page caching. 2024-12-28 16:39:43 +01:00
Benjin Dubishar
8e129c561d Some miscellaneous cleanup (#494)
* Updating CONTRIBUTING guidelines

* Adding PackageCertificateKeyFile ref to Calendar proj to allow building on fresh machines

* Adding missing conversions to MailSync type
2024-12-27 20:27:45 +01:00
Burak Kaan Köse
fbc3ca4517 Synchronizing calendars for gmail and some events. 2024-12-27 00:18:46 +01:00
Burak Kaan Köse
1668dfcce6 More abstraction for mail/calendar. 2024-12-24 18:30:25 +01:00
Burak Kaan Köse
da2a58a88b Fix focus-other toggle for Live accounts. 2024-12-22 00:59:59 +01:00
Burak Kaan Köse
8390a868ba Bump release version. 2024-12-22 00:50:01 +01:00
Burak Kaan Köse
296c498464 Fixing serial execution for sending drafts in Outlook. 2024-12-22 00:49:55 +01:00
Kamil
a92ff89221 Implement clickable plaintext links and dark mode fix (#488)
* Plaintext links are now clickable and fixes dark mode.

- Change `AppxPackageDir` path from `C:\Users\bkaan\Desktop\Packages\` to `$(USERPROFILE)\Desktop\Packages\`, fixes error when building release.
- Plaintext links are now clickable, and match the same subtle style as Windows Mail.
- Remove `!important` from inline styles to allow Dark Reader to properly recolor the element.

* Implement setting for clickable plaintext link
2024-12-21 20:39:03 +01:00
Burak Kaan Köse
e3b2f41a1c Bumping up some nugets. 2024-12-21 20:30:48 +01:00
Burak Kaan Köse
9b3424fa90 Fixing build errors and gmail profile synchronization. 2024-12-21 19:31:24 +01:00
Burak Kaan Köse
678d947f16 Fixing profile sync for gmail and separating authenticators token storage. 2024-12-01 03:05:15 +01:00
Burak Kaan Köse
0cd1568c64 Separation of core library from the UWP app. 2024-11-30 23:05:07 +01:00
Burak Kaan Köse
4e25dbf5e3 Remove redundant extensions folder. 2024-11-30 12:47:46 +01:00
Burak Kaan Köse
14e10c038c Single synchronizer for calendar and mail. 2024-11-30 12:47:24 +01:00
Burak Kaan Köse
2bc5be2105 Compress command for IMAP after the authentication. 2024-11-30 11:48:05 +01:00
Burak Kaan Köse
36b8de470a Fix delete key executing twice on mail list. 2024-11-27 20:43:49 +01:00
Burak Kaan Köse
9abe3dd7b3 Request binding for each request bundle. 2024-11-27 20:32:17 +01:00
Burak Kaan Köse
96c98a6987 New attachment templates that support saving and opening attachment when composing message. 2024-11-27 19:49:10 +01:00
Burak Kaan Köse
e586145f50 Fix invalid deserialization during folder rename. 2024-11-27 19:48:50 +01:00
Burak Kaan Köse
611fbfa6df Fix grammer issue with english translation. 2024-11-27 18:40:55 +01:00
Burak Kaan Köse
54eb4f78b2 More abstractions 2024-11-27 03:01:14 +01:00
Burak Kaan Köse
1ce7bb8c02 Seperation of base synchronizer. 2024-11-27 02:51:07 +01:00
Burak Kaan Köse
ef17e86465 Fix attachment icons. 2024-11-27 02:30:32 +01:00
Burak Kaan Köse
cd3e0492f5 Fix themes and attachment icons. 2024-11-27 01:43:03 +01:00
Burak Kaan Köse
c3f98e327c Implement custom wrap panel for reading page. 2024-11-27 01:42:50 +01:00
Burak Kaan Köse
20fc34a6fd Replace progress bar with progress ring for sync progress. 2024-11-26 23:44:32 +01:00
Burak Kaan Köse
87af67c36c Fix for missing attachmnet file icon styles in core library and title bar drag area. 2024-11-26 23:13:11 +01:00
Burak Kaan Köse
f33335a768 Missing folder operation changes. 2024-11-26 20:04:25 +01:00
Burak Kaan Köse
a9fffd44d2 New change request abstractions. 2024-11-26 20:03:10 +01:00
Burak Kaan Köse
e81b7e2e61 Remove base mail integrator. 2024-11-25 17:42:18 +01:00
Burak Kaan Köse
7fad15524f Abstraction of authenticators. Reworked Gmail authentication. 2024-11-20 01:45:48 +01:00
Burak Kaan Köse
8367efa174 Move wino icons font to shared project. 2024-11-11 14:04:22 +01:00
Burak Kaan Köse
aa86b7efff Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-11-11 13:57:00 +01:00
Burak Kaan Köse
b490450107 Server connectivity for calendar and some abstraction for server. 2024-11-11 13:56:56 +01:00
Xinyang Chen
a4a7ff46c5 Fixes mail header encoding issues with MimeKit (#450)
For details see #441
2024-11-11 11:42:19 +01:00
Burak Kaan Köse
418eeb7317 Calendar account management page basics. 2024-11-11 01:09:05 +01:00
Burak Kaan Köse
5b0fcd77e5 Seperation of root account template. 2024-11-11 00:01:42 +01:00
Burak Kaan Köse
757a73ca6b Fix wino mail build error. 2024-11-11 00:01:25 +01:00
Burak Kaan Köse
faab29cab7 Fix calendar build error. 2024-11-10 23:37:56 +01:00
Burak Kaan Köse
d1d6f12f05 Ground work for Wino Calendar. (#475)
Wino Calendar abstractions.
2024-11-10 23:28:25 +01:00
Burak Kaan Köse
a979e8430f Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-11-09 20:48:48 +01:00
Burak Kaan Köse
f5f045e8f1 Fix issue when account count is miscalculated for free access. 2024-11-09 20:48:43 +01:00
Burak Kaan Köse
90e291ac8a Allow disabling badge update per-account. (#472) 2024-11-09 20:37:38 +01:00
Burak Kaan Köse
b49e1b3a97 Printing mails. (#471)
* Implemented printing functionality.

* Implemented icon for printing.

* Remove debug code.
2024-11-09 19:18:06 +01:00
Burak Kaan Köse
5245feb739 New Crowdin updates (#463)
* 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)
2024-11-03 15:54:39 +01:00
Tiktack
550c8fb899 Replace T4 with Source Generator (#459)
* Replace T4 template with source generator

* remove space

* Added summary

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2024-11-03 15:51:08 +01:00
Xinyang Chen
89ffc5d246 Ensure client capabilities are enforced on reconnection (#404)
* Ensure client capabilities are enforced on reconnection

* Cleanup bolierplate
2024-11-03 15:47:33 +01:00
dabardibid
3330873ce0 Update NotificationBuilder.cs (#461)
Mail notifications now use the default Notification Email sound from Windows, just like in the "legacy" Mail app.
2024-11-03 15:46:30 +01:00
Tiktack
ff998a8bd1 Fix for To/Cc/Bcc alignment and crush when opening eml (#449)
* Add margin to To/Cc/Bcc

* Fix crash when opening eml

* Remove useless top
2024-11-03 15:45:10 +01:00
Tiktack
a1517f82bc Added no subject handler (#451)
* Added no subject handler
changed text trimming for address to CharacterEllipsis

* Make email subject selectable.
2024-11-03 15:44:39 +01:00
Tiktack
d8885b089a Removed margin from mail list (#456)
Adjusted margin for group header to match new mail list
2024-11-03 15:44:16 +01:00
Tiktack
beba06b8ba Added escaping for line breaks in mailto body to <br> (#419) 2024-10-21 21:03:00 +02:00
Tiktack
762be492bb Added flyout on click for recipients (#436)
User accoutn will be first in recipents list
Recipient shows eather name or address to save space
Added tooltip which shows both ( Name and Address)
Added ";" to have visual separation between names, since we don't have address all the time.
To/Cc/Bcc now at the top of their container, previously they were center and it was hard to understand is it To/CC/Bcc recipient when there many of them
2024-10-21 21:02:02 +02:00
Tiktack
c00efff554 Scroll window to the top on render. (#423) 2024-10-21 21:00:32 +02:00
Burak Kaan Köse
5258ae4b34 Release version bump 2024-10-01 19:10:09 +02:00
Burak Kaan Köse
a8b19e73fe ImapClientPoolOptions 2024-09-29 21:21:51 +02:00
Burak Kaan Köse
92944c7adc Potential crash for folder sync. 2024-09-29 20:42:03 +02:00
Burak Kaan Köse
0042173ddc Prevent potential crash on threads, somehow... 2024-09-29 13:49:02 +02:00
Burak Kaan Köse
a438b5ba17 Fixing regular accounts for mail add check. 2024-09-29 13:37:20 +02:00
Xinyang Chen
b86643c052 Fix profile fetch permission issues for Exchange (#401) 2024-09-29 00:45:59 +02:00
Burak Kaan Köse
e897182b23 Adding default secure ports to advanced imap setup dialog. 2024-09-27 01:29:56 +02:00
Burak Kaan Köse
56329f02b6 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-27 01:26:19 +02:00
Burak Kaan Köse
11ab579de9 Fixing too many recipients blocking the mail rendering issue. 2024-09-27 01:14:37 +02:00
Tiktack
9aa1de11af Added more handling when fromName is null or empty (#399) 2024-09-27 01:06:57 +02:00
Tiktack
939b395dcd Replace old markdown and remove redundant events (#389)
* Remove redundant messages

* Replaced old markdown with preview.
Added nuget config to add preview feed

* Fix old extensions usage

* Scrollbar margin for the markdown.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2024-09-27 00:46:51 +02:00
Burak Kaan Köse
118e9bf50b Fixing invalid logic when the e-mail recieved. 2024-09-27 00:13:36 +02:00
Burak Kaan Köse
67828365ca New Crowdin updates (#390)
* 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 (Romanian)
2024-09-23 18:37:50 +02:00
Burak Kaan Köse
e0d99257fe Skipping unknown tags in mime visitor. 2024-09-19 01:16:42 +02:00
Tiktack
e628a98cb8 Fixed visual bug with margin in narrow state (#383)
* Reworked paddings/margins for mail list

* Fix composer page

* Moved layput changes from code behind to visual states
2024-09-19 00:52:27 +02:00
Burak Kaan Köse
a4f9284970 Making sure that server is terminated even though there are no accounts. 2024-09-15 00:30:08 +02:00
Burak Kaan Köse
c403a716dd Non-message item check for only non-existing messages. 2024-09-14 22:23:12 +02:00
Burak Kaan Köse
4278c8bacb Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-14 22:22:53 +02:00
Burak Kaan Köse
56bfbeca58 SSL Handshake Prompt for IMAP (#381)
* Fix an incorrect namespace for copy auth url request.

* Implemented SSL handshake process for testing imap configuration.

* Implemented SSL handshake process for testing imap configuration.

* Replace certificate PathIcon with WinoFontIcon in XAML.
2024-09-14 21:51:43 +02:00
Burak Kaan Köse
b6f9eae7b5 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-14 21:17:23 +02:00
Tiktack
cad9250cb7 Feature/update icons (#378)
* Added a few icons to ttf font

* Replace junk with blocked

* Fix build

* Remove outdated commandBar styles

* Added Icomoon file to root

* Added imap icon
2024-09-14 21:15:38 +02:00
Tiktack
4ac8095554 Fixes Webview jumps when navigating emails and added Tooltips to the collapsed nav view. (#373)
* Prepare To/Cc/Bcc info in advance to avoid layout shifts

* Changed tooltips in nav view to apply to whole element instead of content

* Revert comment
2024-09-14 18:41:33 +02:00
Burak Kaan Köse
05d16983eb Fix an incorrect namespace for copy auth url request. 2024-09-14 05:08:30 +02:00
Burak Kaan Köse
12f821fd6b Test certificate 2024-09-14 01:42:55 +02:00
Burak Kaan Köse
13c8bf5f19 Prevent crashes on resume due to connectivity issues. 2024-09-14 01:33:35 +02:00
Burak Kaan Köse
9a44e30e0f Ability to copying authorization URL for Gmail (#375)
* Implemented copying auth URL for Gmail authentication.

* Update Button icon and add row spacing in Flyout grid

The icon used in the Button.Content has been updated to a new
design and is now wrapped inside a Viewbox with a width of 20
to ensure proper scaling. Additionally, the Grid inside the
Flyout now includes RowSpacing="12" to improve visual separation
between rows.
2024-09-14 01:17:03 +02:00
Burak Kaan Köse
bf77572041 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-13 02:56:34 +02:00
Burak Kaan Köse
18ba4851d1 Release 1.9.2.0. 2024-09-13 02:56:28 +02:00
Burak Kaan Köse
6620034d98 New Crowdin updates (#368)
* 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 (Romanian)
2024-09-13 02:54:22 +02:00
Burak Kaan Köse
e93ecc7e4a Canceling authentication and Outlook improvements (#367)
* Cancellation support for getting responses from the server.

* Adding cancel button for account creation dialog initialization.

* Prevent invalid outlook message types like contact, calendar event or todo item.

* Remove debug launcher options.
2024-09-13 02:51:37 +02:00
Burak Kaan Köse
f85085de41 Add support for custom names for aliases. Synchronization of names for the gmail api. (#365) 2024-09-12 01:14:40 +02:00
Burak Kaan Köse
310943590b Don't crash on Outlook profile picture fetch. 2024-09-12 00:50:49 +02:00
Burak Kaan Köse
c1dcd52a28 Don't crash when StartupTask is not present. 2024-09-12 00:08:31 +02:00
Burak Kaan Köse
9bee5e449f Updating unread badge count from server. 2024-09-12 00:02:14 +02:00
Tiktack
02e99066ca Hotfix: Change FontIcon to PathIcon/SymbolIcon for Win10 users. (#358)
* Replaced Segoe Icons with Path/Symbol

* Add tooltips
2024-09-10 17:20:14 +02:00
Tiktack
7761cf6dbe Code Quality: Fixed multiple paddings, disabled backspace addon (#354)
* Changed paddings and size for unsubscribe button to avoid shifts

* Fix bottom paddings

* Disable backspace addon, simplify attachments logic, convert switches

* Remove redundant code from Mail Rendering page

* Replace Font Icon to Path
2024-09-10 10:14:13 +02:00
Burak Kaan Köse
7715d4bdda Fixing an issue where replacing folders with expanded items causing unmanaged crash. 2024-09-05 18:11:50 +02:00
Burak Kaan Köse
f3c4906f88 Fixing Outlook attachment issues. 2024-09-05 17:23:15 +02:00
Burak Kaan Köse
3dc16fa07b Fix romanian language. 2024-09-03 15:40:16 +02:00
Burak Kaan Köse
be58bdf24f New Crowdin updates (#347)
* 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 (Romanian)
2024-09-03 15:38:00 +02:00
Burak Kaan Köse
ac170c67bf Enable Turkish and Romanian translations. 2024-09-03 15:35:30 +02:00
Burak Kaan Köse
169cd9d743 Disable tree view re orderings. 2024-09-03 15:34:42 +02:00
Burak Kaan Köse
a1931f08a8 Remove building tools for packaging project. 2024-09-03 11:49:01 +02:00
Burak Kaan Köse
5617206c6d Spam filter check per-message. 2024-09-03 11:48:46 +02:00
Burak Kaan Köse
cee00b8b2b Store release 1.9.0. 2024-09-03 03:12:52 +02:00
Burak Kaan Köse
323a8acbd5 Fallback to welcome page if startup entity is not found. 2024-09-03 01:49:45 +02:00
Burak Kaan Köse
d488b10848 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-03 01:39:57 +02:00
Burak Kaan Köse
24c99364ef Fix incorrect title-message for missing account dialog. 2024-09-03 01:38:52 +02:00
Burak Kaan Köse
f8b6975e70 Fixing serialization issue with MimeMessage. 2024-09-02 22:19:26 +02:00
Burak Kaan Köse
c416e8c1fb Merge pull request #346 from bkaankose/fix/centering-and-tooltips
Center new Segmented component and add tooltips to action bar
2024-09-01 11:26:16 +02:00
Aleh Khantsevich
51626dfd04 Remove redundand command bar classes 2024-09-01 01:28:36 +02:00
Aleh Khantsevich
cb05e58f1e Center Sigmented and add tooltips 2024-09-01 01:12:00 +02:00
Burak Kaan Köse
24c8cfd402 Revert padding for mail list container. 2024-08-31 18:23:22 +02:00
Burak Kaan Köse
8257b0b582 Merge pull request #345 from bkaankose/hotfix/Expander
New expander control for conversation threads
2024-08-31 16:12:32 +02:00
Burak Kaan Köse
e612f2c281 Remove redundant code. 2024-08-31 16:11:19 +02:00
Burak Kaan Köse
155df59b1d Increase pre-defined size to fit mail icon changes. 2024-08-31 16:11:11 +02:00
Burak Kaan Köse
6efab9f386 merged main 2024-08-31 15:31:29 +02:00
Burak Kaan Köse
422105a507 Merge pull request #344 from bkaankose/features/action-bar
Mail List action bar improvements
2024-08-31 15:28:59 +02:00
Burak Kaan Köse
d58438ab1d Removed margin and increased padding. 2024-08-31 15:26:22 +02:00
Burak Kaan Köse
209aa1a89f 4x top margin for mail list. 2024-08-31 15:22:20 +02:00
Aleh Khantsevich
07ac81583e Fix delete action on KeyPress 2024-08-31 15:11:28 +02:00
Burak Kaan Köse
ee6249bb17 Selecting first mail when thread is expanded. 2024-08-31 15:08:43 +02:00
Aleh Khantsevich
b8ca3f8604 Disable toolbar when nothing selected. 2024-08-31 14:29:57 +02:00
Aleh Khantsevich
85b5469d96 Fix wrong logic for set/unset flag 2024-08-31 14:19:43 +02:00
Aleh Khantsevich
d3ddf7b191 style/renaming 2024-08-31 13:56:31 +02:00
Aleh Khantsevich
ebf196ec73 Remove redundant message 2024-08-31 13:39:32 +02:00
Aleh Khantsevich
85c3833452 Make TopBar dynamic 2024-08-31 13:25:55 +02:00
Burak Kaan Köse
8fb4735fc2 Handling hover action tap delegates. 2024-08-31 03:37:04 +02:00
Burak Kaan Köse
6bb09f10d2 Finished wino expander implementation. 2024-08-31 03:18:43 +02:00
Aleh Khantsevich
a4ff67e8f4 Fix margins 2024-08-31 00:42:17 +02:00
Burak Kaan Köse
b9a1756f90 Moving expander chevron to mail display control. 2024-08-30 02:20:16 +02:00
Burak Kaan Köse
72ff8e67ed Merge branch 'main' into hotfix/Expander 2024-08-30 01:11:19 +02:00
Burak Kaan Köse
d7006365eb Merge pull request #342 from bkaankose/hotfix/InvalidContactCrashFix
Fixed invalid contacts causing folder loading to crash.
2024-08-30 01:05:01 +02:00
Burak Kaan Köse
86ef78b296 Fixed invalid contacts causing folder loading to crash. 2024-08-30 01:03:35 +02:00
Aleh Khantsevich
0d84e409c5 Fix button sizes 2024-08-30 00:15:13 +02:00
Burak Kaan Köse
17c7b33167 Merge pull request #341 from bkaankose/hotfix/FixGmailDelete
Fixing Gmail SENT label deletes.
2024-08-30 00:03:53 +02:00
Burak Kaan Köse
856e1613a0 Fixing Gmail sent folder deletes. 2024-08-29 23:58:39 +02:00
Burak Kaan Köse
8db34289a7 Fixing single threads for API threading strategy. 2024-08-29 23:43:49 +02:00
Burak Kaan Köse
3016f70349 Handling gmail errors. 2024-08-29 22:43:27 +02:00
Aleh Khantsevich
bdf212fdb3 Remove bottom padding of mail list 2024-08-29 17:36:50 +02:00
Aleh Khantsevich
c6216f54f8 simplify layout for mail list 2024-08-29 17:18:46 +02:00
Aleh Khantsevich
945c747e3e Added setting to show/hide action bar in mail list 2024-08-29 01:13:51 +02:00
Aleh Khantsevich
552fca8df7 Moved refresh and multi select buttons 2024-08-29 00:19:24 +02:00
Burak Kaan Köse
4dac160619 Fix 995 char limit on message headers for Outlook. 2024-08-28 22:17:13 +02:00
Burak Kaan Köse
fc0e746e1b Reduce outlook send draft delay to 6 secs. 2024-08-27 01:48:03 +02:00
Burak Kaan Köse
8374b5fc0c Fix replying issues with Outlook due to cc or bcc headers. 2024-08-27 01:47:50 +02:00
Burak Kaan Köse
52923ed35b Bump some more nugets, remove redundant events, and fix Outlook profile sync permission issue. 2024-08-26 22:09:00 +02:00
Aleh Khantsevich
f002ccfa3a Replace WinoPivot with segmented 2024-08-26 17:27:27 +02:00
Burak Kaan Köse
f4bbf6eb73 New expander control. 2024-08-26 01:07:51 +02:00
1198 changed files with 60487 additions and 44025 deletions

View File

@@ -149,7 +149,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 +287,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

3
.gitignore vendored
View File

@@ -206,9 +206,6 @@ PublishScripts/
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef

View File

@@ -15,7 +15,12 @@ Wino Mail is [Universal Windows Platform](https://learn.microsoft.com/en-us/wind
**Min Version:** Windows 10 1809
**Target Version:** Windows 11 22H2
It's pretty straightforward after cloning the repo. There are no prerequisites needed. Just open **Wino.sln** solution in your IDE and launch.
## Prerequisites
* ".NET desktop development" workload in Visual Studio 2022+
* .NET SDK 8.0+
With those installed, it's pretty straightforward after cloning the repo. Just open **Wino.sln** solution in your IDE and launch.
## Project Architecture

7
Directory.Build.Props Normal file
View File

@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
<IsAotCompatible>true</IsAotCompatible>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
</Project>

68
Directory.Packages.props Normal file
View File

@@ -0,0 +1,68 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="ColorHashSharp" Version="1.0.0" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Labs.Uwp.Controls.MarkdownTextBlock" Version="0.1.250206-build.2040" />
<PackageVersion Include="CommunityToolkit.Labs.Uwp.DependencyPropertyGenerator" Version="0.1.250206-build.2040" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Uwp.Animations" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Behaviors" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Segmented" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.SettingsControls" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Sizers" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.TabbedCommandBar" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.TokenizingTextBox" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Primitives" Version="8.2.250129-preview2" />
<PackageVersion Include="EmailValidation" Version="1.3.0" />
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.0" />
<PackageVersion Include="Ical.Net" Version="4.3.1" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Graph" Version="5.75.0" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.70.1" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.70.1" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.70.1" />
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
<PackageVersion Include="Microsoft.UI.Xaml" Version="2.8.7" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Uwp.Managed" Version="3.0.0" />
<PackageVersion Include="MimeKit" Version="4.11.0" />
<PackageVersion Include="morelinq" Version="4.4.0" />
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageVersion Include="NodaTime" Version="3.2.2" />
<PackageVersion Include="Sentry.Serilog" Version="5.12.0" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="SqlKata" Version="4.0.1" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.4" />
<PackageVersion Include="System.Text.Json" Version="9.0.4" />
<PackageVersion Include="Win2D.uwp" Version="1.28.2" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageVersion Include="Google.Apis.Auth" Version="1.69.0" />
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.69.0.3667" />
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" />
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.68.0.3359" />
<PackageVersion Include="HtmlKit" Version="1.2.0" />
<PackageVersion Include="MailKit" Version="4.11.0" />
<PackageVersion Include="TimePeriodLibrary.NET" Version="2.1.6" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Authentication;
public abstract class BaseAuthenticator
{
public abstract MailProviderType ProviderType { get; }
protected IAuthenticatorConfig AuthenticatorConfig { get; }
protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
{
AuthenticatorConfig = authenticatorConfig;
}
}

View File

@@ -0,0 +1,50 @@
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Util.Store;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication;
public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
{
public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig)
{
}
public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId;
public bool ProposeCopyAuthURL { get; set; }
public override MailProviderType ProviderType => MailProviderType.Gmail;
/// <summary>
/// Generates the token information for the given account.
/// For gmail, interactivity is automatically handled when you get the token.
/// </summary>
/// <param name="account">Account to get token for.</param>
public Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
=> GetTokenInformationAsync(account);
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
{
var userCredential = await GetGoogleUserCredentialAsync(account);
if (userCredential.Token.IsStale)
{
await userCredential.RefreshTokenAsync(CancellationToken.None);
}
return new TokenInformationEx(userCredential.Token.AccessToken, account.Address);
}
private Task<UserCredential> GetGoogleUserCredentialAsync(MailAccount account)
{
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
{
ClientId = ClientId
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;
using Microsoft.Identity.Client.Extensions.Msal;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication;
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
{
private const string TokenCacheFileName = "OutlookCache.bin";
private bool isTokenCacheAttached = false;
// Outlook
private const string Authority = "https://login.microsoftonline.com/common";
public override MailProviderType ProviderType => MailProviderType.Outlook;
private readonly IPublicClientApplication _publicClientApplication;
private readonly IApplicationConfiguration _applicationConfiguration;
public OutlookAuthenticator(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
{
_applicationConfiguration = applicationConfiguration;
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
Title = "Wino Mail",
ListOperatingSystemAccounts = true,
};
var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
.WithBroker(options)
.WithDefaultRedirectUri()
.WithAuthority(Authority);
_publicClientApplication = outlookAppBuilder.Build();
}
public string[] Scope => AuthenticatorConfig.OutlookScope;
private async Task EnsureTokenCacheAttachedAsync()
{
if (!isTokenCacheAttached)
{
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
isTokenCacheAttached = true;
}
}
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
{
await EnsureTokenCacheAttachedAsync();
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(
a => string.Equals(a.Username?.Trim(), account.Address?.Trim(), StringComparison.OrdinalIgnoreCase));
if (storedAccount == null)
return await GenerateTokenInformationAsync(account);
try
{
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
catch (MsalUiRequiredException)
{
// Somehow MSAL is not able to refresh the token silently.
// Force interactive login.
return await GenerateTokenInformationAsync(account);
}
catch (Exception)
{
throw;
}
}
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
{
try
{
await EnsureTokenCacheAttachedAsync();
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. If you are signing with a Office365, it is not officially supported yet.");
}
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
catch (MsalClientException msalClientException)
{
if (msalClientException.ErrorCode == "authentication_canceled" || msalClientException.ErrorCode == "access_denied")
throw new AccountSetupCanceledException();
throw;
}
throw new AuthenticationException(Translator.Exception_UnknowErrorDuringAuthentication, new Exception(Translator.Exception_TokenGenerationFailed));
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<RootNamespace>Wino.Authentication</RootNamespace>
<Platforms>x86;x64;arm64</Platforms>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Google.Apis.Auth" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="Microsoft.Identity.Client.Broker" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" />
<PackageReference Include="Sentry.Serilog" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup>
</Project>

View File

@@ -86,6 +86,9 @@
<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>

View File

@@ -0,0 +1,37 @@
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation;
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 void EditAccountDetails()
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsEditAccountDetails_Title, WinoPage.EditAccountDetailsPage, Account));
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
}
}

View File

@@ -0,0 +1,146 @@
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);
await accountCreationDialog.ShowDialogAsync(accountCreationCancellationTokenSource);
await Task.Delay(500);
// For OAuth authentications, we just generate token and assign it to the MailAccount.
var createdAccount = new MailAccount()
{
ProviderType = accountCreationDialogResult.ProviderType,
Name = accountCreationDialogResult.AccountName,
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();
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));
}
}

View File

@@ -0,0 +1,366 @@
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;
});
}
}

View File

@@ -0,0 +1,870 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using MoreLinq;
using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Calendar.ViewModels.Messages;
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.Models.Calendar;
using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels;
public partial class CalendarPageViewModel : CalendarBaseViewModel,
IRecipient<LoadCalendarMessage>,
IRecipient<CalendarItemDeleted>,
IRecipient<CalendarSettingsUpdatedMessage>,
IRecipient<CalendarItemTappedMessage>,
IRecipient<CalendarItemDoubleTappedMessage>,
IRecipient<CalendarItemRightTappedMessage>
{
#region Quick Event Creation
[ObservableProperty]
private bool _isQuickEventDialogOpen;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private AccountCalendarViewModel _selectedQuickEventAccountCalendar;
public string SelectedQuickEventAccountCalendarName
{
get
{
return SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name;
}
}
[ObservableProperty]
private List<string> _hourSelectionStrings;
// To be able to revert the values when the user enters an invalid time.
private string _previousSelectedStartTimeString;
private string _previousSelectedEndTimeString;
[ObservableProperty]
private DateTime? _selectedQuickEventDate;
[ObservableProperty]
private bool _isAllDay;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private string _selectedStartTimeString;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private string _selectedEndTimeString;
[ObservableProperty]
private string _location;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private string _eventName;
public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedStartTimeString).Value);
public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedEndTimeString).Value);
public bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null &&
!string.IsNullOrWhiteSpace(EventName) &&
!string.IsNullOrWhiteSpace(SelectedStartTimeString) &&
!string.IsNullOrWhiteSpace(SelectedEndTimeString) &&
QuickEventEndTime > QuickEventStartTime;
#endregion
#region Data Initialization
[ObservableProperty]
private CalendarOrientation _calendarOrientation = CalendarOrientation.Horizontal;
[ObservableProperty]
private DayRangeCollection _dayRanges = [];
[ObservableProperty]
private int _selectedDateRangeIndex;
[ObservableProperty]
private DayRangeRenderModel _selectedDayRange;
[ObservableProperty]
private bool _isCalendarEnabled = true;
#endregion
#region Event Details
public event EventHandler DetailsShowCalendarItemChanged;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))]
private CalendarItemViewModel _displayDetailsCalendarItemViewModel;
public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null;
#endregion
// TODO: Get rid of some of the items if we have too many.
private const int maxDayRangeSize = 10;
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
private readonly IKeyPressService _keyPressService;
private readonly IPreferencesService _preferencesService;
// Store latest rendered options.
private CalendarDisplayType _currentDisplayType;
private int _displayDayCount;
private SemaphoreSlim _calendarLoadingSemaphore = new(1);
private bool isLoadMoreBlocked = false;
[ObservableProperty]
private CalendarSettings _currentSettings;
public IStatePersistanceService StatePersistanceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
public CalendarPageViewModel(IStatePersistanceService statePersistanceService,
ICalendarService calendarService,
INavigationService navigationService,
IKeyPressService keyPressService,
IAccountCalendarStateService accountCalendarStateService,
IPreferencesService preferencesService)
{
StatePersistanceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService;
_calendarService = calendarService;
_navigationService = navigationService;
_keyPressService = keyPressService;
_preferencesService = preferencesService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
}
private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
=> FilterActiveCalendars(DayRanges);
private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
=> FilterActiveCalendars(DayRanges);
private async void FilterActiveCalendars(IEnumerable<DayRangeRenderModel> dayRangeRenderModels)
{
await ExecuteUIThread(() =>
{
var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays);
days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id)));
DisplayDetailsCalendarItemViewModel = null;
});
}
// TODO: Replace when calendar settings are updated.
// Should be a field ideally.
private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType)
{
return displayType switch
{
CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings),
CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings),
CalendarDisplayType.Month => new MonthCalendarDrawingStrategy(CurrentSettings),
_ => throw new NotImplementedException(),
};
}
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
// Do not call base method because that will unregister messenger recipient.
// This is a singleton view model and should not be unregistered.
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
if (mode == NavigationMode.Back) return;
RefreshSettings();
// Automatically select the first primary calendar for quick event dialog.
SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary);
}
[RelayCommand]
private void NavigateSeries()
{
if (DisplayDetailsCalendarItemViewModel == null) return;
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Series);
}
[RelayCommand]
private void NavigateEventDetails()
{
if (DisplayDetailsCalendarItemViewModel == null) return;
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Single);
}
private void NavigateEvent(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType calendarEventTargetType)
{
var target = new CalendarItemTarget(calendarItemViewModel.CalendarItem, calendarEventTargetType);
_navigationService.Navigate(WinoPage.EventDetailsPage, target);
}
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
private async Task SaveQuickEventAsync()
{
var durationSeconds = (QuickEventEndTime - QuickEventStartTime).TotalSeconds;
var testCalendarItem = new CalendarItem
{
CalendarId = SelectedQuickEventAccountCalendar.Id,
StartDate = QuickEventStartTime,
DurationInSeconds = durationSeconds,
CreatedAt = DateTime.UtcNow,
Description = string.Empty,
Location = Location,
Title = EventName,
Id = Guid.NewGuid()
};
IsQuickEventDialogOpen = false;
await _calendarService.CreateNewCalendarItemAsync(testCalendarItem, null);
// TODO: Create the request with the synchronizer.
}
[RelayCommand]
private void MoreDetails()
{
// TODO: Navigate to advanced event creation page with existing parameters.
}
public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime)
{
IsAllDay = false;
SelectedStartTimeString = CurrentSettings.GetTimeString(startTime);
SelectedEndTimeString = CurrentSettings.GetTimeString(endTime);
}
// Manage event detail popup context and select-unselect the proper items.
// Item selection rules are defined in the selection method.
partial void OnDisplayDetailsCalendarItemViewModelChanging(CalendarItemViewModel oldValue, CalendarItemViewModel newValue)
{
if (oldValue != null)
{
UnselectCalendarItem(oldValue);
}
if (newValue != null)
{
SelectCalendarItem(newValue);
}
}
// Notify view that the detail context changed.
// This will align the event detail popup to the selected event.
partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value)
=> DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty);
private void RefreshSettings()
{
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
// Populate the hour selection strings.
var timeStrings = new List<string>();
for (int hour = 0; hour < 24; hour++)
{
for (int minute = 0; minute < 60; minute += 30)
{
var time = new DateTime(1, 1, 1, hour, minute, 0);
if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
{
timeStrings.Add(time.ToString("HH:mm"));
}
else
{
timeStrings.Add(time.ToString("h:mm tt"));
}
}
}
HourSelectionStrings = timeStrings;
}
partial void OnIsCalendarEnabledChanging(bool oldValue, bool newValue) => Messenger.Send(new CalendarEnableStatusChangedMessage(newValue));
private bool ShouldResetDayRanges(LoadCalendarMessage message)
{
if (message.ForceRedraw) return true;
// Never reset if the initiative is from the app.
if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
// 1. Display type is different.
// 2. Day display count is different.
// 3. Display date is not in the visible range.
if (DayRanges.DisplayRange == null) return false;
return
(_currentDisplayType != StatePersistanceService.CalendarDisplayType ||
_displayDayCount != StatePersistanceService.DayDisplayCount ||
!(message.DisplayDate >= DayRanges.DisplayRange.StartDate && message.DisplayDate <= DayRanges.DisplayRange.EndDate));
}
private void AdjustCalendarOrientation()
{
// Orientation only changes when we should reset.
// Handle the FlipView orientation here.
// We don't want to change the orientation while the item manipulation is going on.
// That causes a glitch in the UI.
bool isRequestedVerticalCalendar = StatePersistanceService.CalendarDisplayType == CalendarDisplayType.Month;
bool isLastRenderedVerticalCalendar = _currentDisplayType == CalendarDisplayType.Month;
if (isRequestedVerticalCalendar && !isLastRenderedVerticalCalendar)
{
CalendarOrientation = CalendarOrientation.Vertical;
}
else
{
CalendarOrientation = CalendarOrientation.Horizontal;
}
}
public async void Receive(LoadCalendarMessage message)
{
await _calendarLoadingSemaphore.WaitAsync();
try
{
await ExecuteUIThread(() => IsCalendarEnabled = false);
if (ShouldResetDayRanges(message))
{
Debug.WriteLine("Will reset day ranges.");
await ClearDayRangeModelsAsync();
}
else if (ShouldScrollToItem(message))
{
// Scroll to the selected date.
Messenger.Send(new ScrollToDateMessage(message.DisplayDate));
Debug.WriteLine("Scrolling to selected date.");
return;
}
AdjustCalendarOrientation();
// This will replace the whole collection because the user initiated a new render.
await RenderDatesAsync(message.CalendarInitInitiative,
message.DisplayDate,
CalendarLoadDirection.Replace);
// Scroll to the current hour.
Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour)));
}
catch (Exception ex)
{
Log.Error(ex, "Error while loading calendar.");
Debugger.Break();
}
finally
{
_calendarLoadingSemaphore.Release();
await ExecuteUIThread(() => IsCalendarEnabled = true);
}
}
private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
{
if (dayRangeRenderModel == null) return;
await ExecuteUIThread(() =>
{
DayRanges.Add(dayRangeRenderModel);
});
}
private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index)
{
if (dayRangeRenderModel == null) return;
await ExecuteUIThread(() =>
{
DayRanges.Insert(index, dayRangeRenderModel);
});
}
private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
{
if (dayRangeRenderModel == null) return;
await ExecuteUIThread(() =>
{
DayRanges.Remove(dayRangeRenderModel);
});
}
private async Task ClearDayRangeModelsAsync()
{
await ExecuteUIThread(() =>
{
DayRanges.Clear();
});
}
private async Task RenderDatesAsync(CalendarInitInitiative calendarInitInitiative,
DateTime? loadingDisplayDate = null,
CalendarLoadDirection calendarLoadDirection = CalendarLoadDirection.Replace)
{
isLoadMoreBlocked = calendarLoadDirection == CalendarLoadDirection.Replace;
// This is the part we arrange the flip view calendar logic.
/* Loading for a month of the selected date is fine.
* If the selected date is in the loaded range, we'll just change the selected flip index to scroll.
* If the selected date is not in the loaded range:
* 1. Detect the direction of the scroll.
* 2. Load the next month.
* 3. Replace existing month with the new month.
*/
// 2 things are important: How many items should 1 flip have, and, where we should start loading?
// User initiated renders must always have a date to start with.
if (calendarInitInitiative == CalendarInitInitiative.User) Guard.IsNotNull(loadingDisplayDate, nameof(loadingDisplayDate));
var strategy = GetDrawingStrategy(StatePersistanceService.CalendarDisplayType);
var displayDate = loadingDisplayDate.GetValueOrDefault();
// How many days should be placed in 1 flip view item?
int eachFlipItemCount = strategy.GetRenderDayCount(displayDate, StatePersistanceService.DayDisplayCount);
DateRange flipLoadRange = null;
if (calendarInitInitiative == CalendarInitInitiative.User || DayRanges.DisplayRange == null)
{
flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount);
}
else
{
// App is trying to load.
// This should be based on direction. We'll load the next or previous range.
// DisplayDate is either the start or end date of the current visible range.
if (calendarLoadDirection == CalendarLoadDirection.Previous)
{
flipLoadRange = strategy.GetPreviousDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
}
else
{
flipLoadRange = strategy.GetNextDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
}
}
// Create day ranges for each flip item until we reach the total days to load.
int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount);
List<DayRangeRenderModel> renderModels = new();
for (int i = 0; i < totalFlipItemCount; i++)
{
var startDate = flipLoadRange.StartDate.AddDays(i * eachFlipItemCount);
var endDate = startDate.AddDays(eachFlipItemCount);
var range = new DateRange(startDate, endDate);
var renderOptions = new CalendarRenderOptions(range, CurrentSettings);
var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions);
renderModels.Add(dayRangeHeaderModel);
}
// Dates are loaded. Now load the events for them.
foreach (var renderModel in renderModels)
{
await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false);
}
// Filter by active calendars. This is a quick operation, and things are not on the UI yet.
FilterActiveCalendars(renderModels);
CalendarLoadDirection animationDirection = calendarLoadDirection;
//bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace;
if (calendarLoadDirection == CalendarLoadDirection.Replace)
{
// New date ranges are being replaced.
// We must preserve existing selection if any, add the items before/after the current one, remove the current one.
// This will make sure the new dates are animated in the correct direction.
isLoadMoreBlocked = true;
// Remove all other dates except this one.
var rangesToRemove = DayRanges.Where(a => a != SelectedDayRange).ToList();
foreach (var range in rangesToRemove)
{
await RemoveDayRangeModelAsync(range);
}
animationDirection = displayDate <= SelectedDayRange?.CalendarRenderOptions.DateRange.StartDate ?
CalendarLoadDirection.Previous : CalendarLoadDirection.Next;
}
if (animationDirection == CalendarLoadDirection.Next)
{
foreach (var item in renderModels)
{
await AddDayRangeModelAsync(item);
}
}
else if (animationDirection == CalendarLoadDirection.Previous)
{
// Wait for the animation to finish.
// Otherwise it somehow shutters a little, which is annoying.
// if (!removeCurrent) await Task.Delay(350);
// Insert each render model in reverse order.
for (int i = renderModels.Count - 1; i >= 0; i--)
{
await InsertDayRangeModelAsync(renderModels[i], 0);
}
}
Debug.WriteLine($"Flip count: ({DayRanges.Count})");
foreach (var item in DayRanges)
{
Debug.WriteLine($"- {item.CalendarRenderOptions.DateRange.ToString()}");
}
//if (removeCurrent)
//{
// await RemoveDayRangeModelAsync(SelectedDayRange);
//}
// TODO...
// await TryConsolidateItemsAsync();
isLoadMoreBlocked = false;
// Only scroll if the render is initiated by user.
// Otherwise we'll scroll to the app rendered invisible date range.
if (calendarInitInitiative == CalendarInitInitiative.User)
{
// Save the current settings for the page for later comparison.
_currentDisplayType = StatePersistanceService.CalendarDisplayType;
_displayDayCount = StatePersistanceService.DayDisplayCount;
Messenger.Send(new ScrollToDateMessage(displayDate));
}
}
protected override async void OnCalendarItemAdded(CalendarItem calendarItem)
{
base.OnCalendarItemAdded(calendarItem);
// Check if event falls into the current date range.
if (DayRanges.DisplayRange == null) return;
// Check whether this event falls into any of the loaded date ranges.
var allDaysForEvent = DayRanges.SelectMany(a => a.CalendarDays).Where(a => a.Period.OverlapsWith(calendarItem.Period));
foreach (var calendarDay in allDaysForEvent)
{
var calendarItemViewModel = new CalendarItemViewModel(calendarItem);
await ExecuteUIThread(() =>
{
calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
});
}
FilterActiveCalendars(DayRanges);
}
private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel)
{
// Clear all events first for all days.
foreach (var day in dayRangeRenderModel.CalendarDays)
{
await ExecuteUIThread(() =>
{
day.EventsCollection.Clear();
});
}
// Initialization is done for all calendars, regardless whether they are actively selected or not.
// This is because the filtering is cached internally of the calendar items in CalendarEventCollection.
var allCalendars = AccountCalendarStateService.GroupedAccountCalendars.SelectMany(a => a.AccountCalendars);
foreach (var calendarViewModel in allCalendars)
{
// Check all the events for the given date range and calendar.
// Then find the day representation for all the events returned, and add to the collection.
var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false);
foreach (var @event in events)
{
// Find the days that the event falls into.
var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
foreach (var calendarDay in allDaysForEvent)
{
var calendarItemViewModel = new CalendarItemViewModel(@event);
await ExecuteUIThread(() =>
{
calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
});
}
}
}
}
private async Task TryConsolidateItemsAsync()
{
// Check if trimming is necessary
if (DayRanges.Count > maxDayRangeSize)
{
Debug.WriteLine("Trimming items.");
isLoadMoreBlocked = true;
var removeCount = DayRanges.Count - maxDayRangeSize;
await Task.Delay(500);
// Right shifted, remove from the start.
if (SelectedDateRangeIndex > DayRanges.Count / 2)
{
DayRanges.RemoveRange(DayRanges.Take(removeCount).ToList());
}
else
{
// Left shifted, remove from the end.
DayRanges.RemoveRange(DayRanges.Skip(DayRanges.Count - removeCount).Take(removeCount));
}
SelectedDateRangeIndex = DayRanges.IndexOf(SelectedDayRange);
}
}
private bool ShouldScrollToItem(LoadCalendarMessage message)
{
// Never scroll if the initiative is from the app.
if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
// Nothing to scroll.
if (DayRanges.Count == 0) return false;
if (DayRanges.DisplayRange == null) return false;
var selectedDate = message.DisplayDate;
return selectedDate >= DayRanges.DisplayRange.StartDate && selectedDate <= DayRanges.DisplayRange.EndDate;
}
partial void OnIsAllDayChanged(bool value)
{
if (value)
{
SelectedStartTimeString = HourSelectionStrings.FirstOrDefault();
SelectedEndTimeString = HourSelectionStrings.FirstOrDefault();
}
else
{
SelectedStartTimeString = _previousSelectedStartTimeString;
SelectedEndTimeString = _previousSelectedEndTimeString;
}
}
partial void OnSelectedStartTimeStringChanged(string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
if (parsedTime == null)
{
SelectedStartTimeString = _previousSelectedStartTimeString;
}
else if (IsAllDay)
{
_previousSelectedStartTimeString = newValue;
}
}
partial void OnSelectedEndTimeStringChanged(string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
if (parsedTime == null)
{
SelectedEndTimeString = _previousSelectedStartTimeString;
}
else if (IsAllDay)
{
_previousSelectedEndTimeString = newValue;
}
}
partial void OnSelectedDayRangeChanged(DayRangeRenderModel value)
{
DisplayDetailsCalendarItemViewModel = null;
if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return;
var selectedRange = DayRanges[SelectedDateRangeIndex];
Messenger.Send(new VisibleDateRangeChangedMessage(new DateRange(selectedRange.Period.Start, selectedRange.Period.End)));
if (isLoadMoreBlocked) return;
_ = LoadMoreAsync();
}
private async Task LoadMoreAsync()
{
try
{
await _calendarLoadingSemaphore.WaitAsync();
// Depending on the selected index, we'll load more dates.
// Day ranges may change while the async update is in progress.
// Therefore we wait for semaphore to be released before we continue.
// There is no need to load more if the current index is not in ideal position.
if (SelectedDateRangeIndex == 0)
{
await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous);
}
else if (SelectedDateRangeIndex == DayRanges.Count - 1)
{
await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next);
}
}
catch (Exception)
{
Debugger.Break();
}
finally
{
_calendarLoadingSemaphore.Release();
}
}
public void Receive(CalendarSettingsUpdatedMessage message)
{
RefreshSettings();
// TODO: This might need throttling due to slider in the settings page for hour height.
// or make sure the slider does not update on each tick but on focus lost.
// Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true));
}
private IEnumerable<CalendarItemViewModel> GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay)
{
// All-day and multi-day events are selected collectively.
// Recurring events must be selected as a single instance.
// We need to find the day that the event is in, and then select the event.
if (!calendarItemViewModel.IsRecurringEvent)
{
return [calendarItemViewModel];
}
else
{
return DayRanges
.SelectMany(a => a.CalendarDays)
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
.Where(c => c != null)
.Cast<CalendarItemViewModel>()
.Distinct();
}
}
private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
{
if (calendarItemViewModel == null) return;
var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);
foreach (var item in itemsToUnselect)
{
item.IsSelected = false;
}
}
private void SelectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
{
if (calendarItemViewModel == null) return;
var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);
foreach (var item in itemsToSelect)
{
item.IsSelected = true;
}
}
public void Receive(CalendarItemTappedMessage message)
{
if (message.CalendarItemViewModel == null) return;
DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
}
public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single);
public void Receive(CalendarItemRightTappedMessage message)
{
}
public async void Receive(CalendarItemDeleted message)
{
// Each deleted recurrence will report for it's own.
await ExecuteUIThread(() =>
{
var deletedItem = message.CalendarItem;
// Event might be spreaded into multiple days.
// Remove from all.
// var calendarItems = GetCalendarItems(deletedItem.Id);
});
}
}

View File

@@ -0,0 +1,126 @@
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());
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Wino.Core;
namespace Wino.Calendar.ViewModels;
public static class CalendarViewModelContainerSetup
{
public static void RegisterCalendarViewModelServices(this IServiceCollection services)
{
services.RegisterCoreServices();
}
}

View File

@@ -0,0 +1,69 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data;
public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar
{
public MailAccount Account { get; }
public AccountCalendar AccountCalendar { get; }
public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar)
{
Account = account;
AccountCalendar = accountCalendar;
IsChecked = accountCalendar.IsExtended;
}
[ObservableProperty]
private bool _isChecked;
partial void OnIsCheckedChanged(bool value) => IsExtended = value;
public string Name
{
get => AccountCalendar.Name;
set => SetProperty(AccountCalendar.Name, value, AccountCalendar, (u, n) => u.Name = n);
}
public string TextColorHex
{
get => AccountCalendar.TextColorHex;
set => SetProperty(AccountCalendar.TextColorHex, value, AccountCalendar, (u, t) => u.TextColorHex = t);
}
public string BackgroundColorHex
{
get => AccountCalendar.BackgroundColorHex;
set => SetProperty(AccountCalendar.BackgroundColorHex, value, AccountCalendar, (u, b) => u.BackgroundColorHex = b);
}
public bool IsExtended
{
get => AccountCalendar.IsExtended;
set => SetProperty(AccountCalendar.IsExtended, value, AccountCalendar, (u, i) => u.IsExtended = i);
}
public bool IsPrimary
{
get => AccountCalendar.IsPrimary;
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
}
public 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 Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data;
public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, ICalendarItemViewModel
{
public CalendarItem CalendarItem { get; }
public string Title => CalendarItem.Title;
public Guid Id => CalendarItem.Id;
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
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)
{
CalendarItem = calendarItem;
}
public override string ToString() => CalendarItem.Title;
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Calendar.ViewModels.Data;
public partial class GroupedAccountCalendarViewModel : ObservableObject
{
public event EventHandler CollectiveSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel> CalendarSelectionStateChanged;
public MailAccount Account { get; }
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
{
Account = account;
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
ManageIsCheckedState();
foreach (var calendarViewModel in calendarViewModels)
{
calendarViewModel.PropertyChanged += CalendarPropertyChanged;
}
AccountCalendars.CollectionChanged += CalendarListUpdated;
}
private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (AccountCalendarViewModel calendar in e.NewItems)
{
calendar.PropertyChanged += CalendarPropertyChanged;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (AccountCalendarViewModel calendar in e.OldItems)
{
calendar.PropertyChanged -= CalendarPropertyChanged;
}
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
foreach (AccountCalendarViewModel calendar in e.OldItems)
{
calendar.PropertyChanged -= CalendarPropertyChanged;
}
}
}
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is AccountCalendarViewModel viewModel)
{
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
{
ManageIsCheckedState();
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
}
}
}
[ObservableProperty]
private bool _isExpanded = true;
[ObservableProperty]
private bool? isCheckedState = true;
private bool _isExternalPropChangeBlocked = false;
private void ManageIsCheckedState()
{
if (_isExternalPropChangeBlocked) return;
_isExternalPropChangeBlocked = true;
if (AccountCalendars.All(c => c.IsChecked))
{
IsCheckedState = true;
}
else if (AccountCalendars.All(c => !c.IsChecked))
{
IsCheckedState = false;
}
else
{
IsCheckedState = null;
}
_isExternalPropChangeBlocked = false;
}
partial void OnIsCheckedStateChanged(bool? 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);
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels;
public partial class EventDetailsPageViewModel : CalendarBaseViewModel
{
private readonly ICalendarService _calendarService;
private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
public CalendarSettings CurrentSettings { get; }
#region Details
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
private CalendarItemViewModel _currentEvent;
[ObservableProperty]
private CalendarItemViewModel _seriesParent;
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
#endregion
public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService)
{
_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;
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Calendar.ViewModels.Interfaces;
public interface IAccountCalendarStateService : INotifyPropertyChanged
{
ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; }
event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void ClearGroupedAccountCalendar();
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; }
}

View File

@@ -0,0 +1,13 @@
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemDoubleTappedMessage
{
public CalendarItemDoubleTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}

View File

@@ -0,0 +1,13 @@
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemRightTappedMessage
{
public CalendarItemRightTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}

View File

@@ -0,0 +1,16 @@
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemTappedMessage
{
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
{
CalendarItemViewModel = calendarItemViewModel;
ClickedPeriod = clickedPeriod;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
public CalendarDayModel ClickedPeriod { get; }
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TimePeriodLibrary.NET" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Core.ViewModels\Wino.Core.ViewModels.csproj" />
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
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;
}

View File

@@ -1,7 +1,31 @@
<Application
<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:local="using:Wino.Calendar">
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>
</Application>

View File

@@ -1,100 +1,159 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
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
namespace Wino.Calendar;
public sealed partial class App : WinoApplication, IRecipient<NewCalendarSynchronizationRequested>
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
sealed partial class App : Application
private BackgroundTaskDeferral connectionBackgroundTaskDeferral;
public App()
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
InitializeComponent();
WeakReferenceMessenger.Default.Register<NewCalendarSynchronizationRequested>(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)
{
this.InitializeComponent();
this.Suspending += OnSuspending;
await ActivateWinoAsync(args);
}
}
/// <summary>
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used such as when the application is launched to open a specific file.
/// </summary>
/// <param name="e">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs e)
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)
{
Frame rootFrame = Window.Current.Content as Frame;
LogActivation("OnBackgroundActivated -> AppServiceTriggerDetails received.");
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
// Only accept connections from callers in the same package
if (appServiceTriggerDetails.CallerPackageFamilyName == Package.Current.Id.FamilyName)
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
// Connection established from the fulltrust process
rootFrame.NavigationFailed += OnNavigationFailed;
connectionBackgroundTaskDeferral = args.TaskInstance.GetDeferral();
args.TaskInstance.Canceled += OnConnectionBackgroundTaskCanceled;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended application
}
AppServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection;
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is active
Window.Current.Activate();
WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstablished());
}
}
}
/// <summary>
/// Invoked when Navigation to a certain page fails
/// </summary>
/// <param name="sender">The Frame which failed navigation</param>
/// <param name="e">Details about the navigation failure</param>
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
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
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(message);
synchronizationResultResponse.ThrowIfFailed();
}
/// <summary>
/// Invoked when application execution is being suspended. Application state is saved
/// without knowing whether the application will be terminated or resumed with the contents
/// of memory still intact.
/// </summary>
/// <param name="sender">The source of the suspend request.</param>
/// <param name="e">Details about the suspend request.</param>
private void OnSuspending(object sender, SuspendingEventArgs e)
catch (WinoServerException serverException)
{
var deferral = e.SuspendingOperation.GetDeferral();
//TODO: Save application state and stop any background activity
deferral.Complete();
var dialogService = Services.GetService<ICalendarDialogService>();
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
}
}
}

View File

@@ -0,0 +1,40 @@
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; }
}

View File

@@ -0,0 +1,8 @@
using System;
namespace Wino.Calendar.Args;
/// <summary>
/// When selected timeline cell is unselected.
/// </summary>
public class TimelineCellUnselectedArgs : EventArgs { }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,30 @@
using Microsoft.UI.Xaml.Controls;
using Windows.UI.Xaml;
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.Controls;
public partial class CalendarItemCommandBarFlyout : CommandBarFlyout
{
public static readonly DependencyProperty ItemProperty = DependencyProperty.Register(nameof(Item), typeof(CalendarItemViewModel), typeof(CalendarItemCommandBarFlyout), new PropertyMetadata(null, new PropertyChangedCallback(OnItemChanged)));
public CalendarItemViewModel Item
{
get { return (CalendarItemViewModel)GetValue(ItemProperty); }
set { SetValue(ItemProperty, value); }
}
private static void OnItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CalendarItemCommandBarFlyout flyout)
{
flyout.UpdateMenuItems();
}
}
private void UpdateMenuItems()
{
}
}

View File

@@ -0,0 +1,160 @@
<UserControl
x:Class="Wino.Calendar.Controls.CalendarItemControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Core.UWP.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Wino.Helpers"
xmlns:local="using:Wino.Calendar.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
d:DesignHeight="300"
d:DesignWidth="400"
CanDrag="True"
DragStarting="ControlDragStarting"
DropCompleted="ControlDropped"
mc:Ignorable="d">
<Grid
x:Name="MainGrid"
CornerRadius="4"
DoubleTapped="ControlDoubleTapped"
RightTapped="ControlRightTapped"
Tapped="ControlTapped"
ToolTipService.ToolTip="{x:Bind CalendarItemTitle, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.ContextFlyout>
<local:CalendarItemCommandBarFlyout Placement="Top">
<local:CalendarItemCommandBarFlyout.PrimaryCommands>
<AppBarButton Icon="Save" Label="save" />
<AppBarButton Icon="Delete" Label="Delet" />
</local:CalendarItemCommandBarFlyout.PrimaryCommands>
</local:CalendarItemCommandBarFlyout>
</Grid.ContextFlyout>
<Grid
x:Name="MainBackground"
Grid.ColumnSpan="2"
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}" />
<Rectangle
x:Name="MainBorder"
Grid.ColumnSpan="2"
Canvas.ZIndex="2"
Stroke="{ThemeResource CalendarItemBorderBrush}"
StrokeThickness="0" />
<TextBlock
x:Name="EventTitleTextblock"
Margin="2,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
CharacterSpacing="8"
FontSize="13"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
HorizontalTextAlignment="Center"
Text="{x:Bind CalendarItemTitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<!-- TODO: Event attributes -->
<StackPanel
x:Name="AttributeStack"
Grid.Column="1"
Margin="0,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal"
Spacing="6">
<controls:WinoFontIcon
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventRepeat"
Visibility="{x:Bind CalendarItem.IsRecurringEvent, Mode=OneWay}" />
<controls:WinoFontIcon
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventMuiltiDay"
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />
</StackPanel>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="NonSelected" />
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Target="MainBorder.StrokeThickness" Value="1" />
<Setter Target="MainBorder.Margin" Value="1" />
<Setter Target="MainBorder.Stroke" Value="{ThemeResource CalendarItemSelectedBorderBrush}" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind CalendarItem.IsSelected, Mode=OneWay, FallbackValue=False}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DraggingStates">
<VisualState x:Name="NotDragging" />
<VisualState x:Name="Dragging">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind IsDragging, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MainBorder.StrokeThickness" Value="2" />
<Setter Target="MainBorder.Stroke" Value="{ThemeResource CalendarItemDraggingBorderBrush}" />
<Setter Target="MainBorder.StrokeDashArray" Value="2.5" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="EventDurationStates">
<!-- Regular event template in the panel. -->
<VisualState x:Name="RegularEvent" />
<!-- All-Day template for top area. -->
<VisualState x:Name="AllDayEvent">
<VisualState.Setters>
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
<Setter Target="MainGrid.MinHeight" Value="30" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Multi-Day template for top area. -->
<VisualState x:Name="CustomAreaMultiDayEvent">
<VisualState.Setters>
<Setter Target="MainBackground.Opacity" Value="1" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
<Setter Target="AttributeStack.Visibility" Value="Visible" />
<Setter Target="AttributeStack.Margin" Value="0,0,4,0" />
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
<Setter Target="MainGrid.MinHeight" Value="30" />
<Setter Target="EventTitleTextblock.HorizontalAlignment" Value="Stretch" />
<Setter Target="EventTitleTextblock.HorizontalTextAlignment" Value="Left" />
<Setter Target="EventTitleTextblock.Margin" Value="6,0" />
</VisualState.Setters>
</VisualState>
<!--
Ghost rendering for multi-day events in the panel.
All-Multi day area template is CustomAreaMultiDayEvent
-->
<VisualState x:Name="MultiDayEvent">
<VisualState.Setters>
<Setter Target="MainGrid.CornerRadius" Value="0" />
<Setter Target="MainBackground.Opacity" Value="0.2" />
<Setter Target="MainGrid.IsHitTestVisible" Value="False" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
<Setter Target="AttributeStack.Visibility" Value="Collapsed" />
<Setter Target="EventTitleTextblock.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,197 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Itenso.TimePeriod;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
public sealed partial class CalendarItemControl : UserControl
{
// Single tap has a delay to report double taps properly.
private bool isSingleTap = false;
public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged)));
public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsCustomEventAreaProperty = DependencyProperty.Register(nameof(IsCustomEventArea), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
public static readonly DependencyProperty CalendarItemTitleProperty = DependencyProperty.Register(nameof(CalendarItemTitle), typeof(string), typeof(CalendarItemControl), new PropertyMetadata(string.Empty));
public static readonly DependencyProperty DisplayingDateProperty = DependencyProperty.Register(nameof(DisplayingDate), typeof(CalendarDayModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnDisplayDateChanged)));
/// <summary>
/// Whether the control is displaying as regular event or all-multi day area in the day control.
/// </summary>
public bool IsCustomEventArea
{
get { return (bool)GetValue(IsCustomEventAreaProperty); }
set { SetValue(IsCustomEventAreaProperty, value); }
}
/// <summary>
/// Day that the calendar item is rendered at.
/// It's needed for title manipulation and some other adjustments later on.
/// </summary>
public CalendarDayModel DisplayingDate
{
get { return (CalendarDayModel)GetValue(DisplayingDateProperty); }
set { SetValue(DisplayingDateProperty, value); }
}
public string CalendarItemTitle
{
get { return (string)GetValue(CalendarItemTitleProperty); }
set { SetValue(CalendarItemTitleProperty, value); }
}
public CalendarItemViewModel CalendarItem
{
get { return (CalendarItemViewModel)GetValue(CalendarItemProperty); }
set { SetValue(CalendarItemProperty, value); }
}
public bool IsDragging
{
get { return (bool)GetValue(IsDraggingProperty); }
set { SetValue(IsDraggingProperty, value); }
}
public CalendarItemControl()
{
InitializeComponent();
}
private static void OnDisplayDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CalendarItemControl control)
{
control.UpdateControlVisuals();
}
}
private static void OnCalendarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CalendarItemControl control)
{
control.UpdateControlVisuals();
}
}
private void UpdateControlVisuals()
{
// Depending on the calendar item's duration and attributes, we might need to change the display title.
// 1. Multi-Day events should display the start date and end date.
// 2. Multi-Day events that occupy the whole day just shows 'all day'.
// 3. Other events should display the title.
if (CalendarItem == null) return;
if (DisplayingDate == null) return;
if (CalendarItem.IsMultiDayEvent)
{
// Multi day events are divided into 3 categories:
// 1. All day events
// 2. Events that started after the period.
// 3. Events that started before the period and finishes within the period.
var periodRelation = CalendarItem.Period.GetRelation(DisplayingDate.Period);
if (periodRelation == Itenso.TimePeriod.PeriodRelation.StartInside ||
periodRelation == PeriodRelation.EnclosingStartTouching)
{
// hour -> title
CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDate.TimeOfDay)} -> {CalendarItem.Title}";
}
else if (
periodRelation == PeriodRelation.EndInside ||
periodRelation == PeriodRelation.EnclosingEndTouching)
{
// title <- hour
CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDate.TimeOfDay)}";
}
else if (periodRelation == PeriodRelation.Enclosing)
{
// This event goes all day and it's multi-day.
// Item must be hidden in the calendar but displayed on the custom area at the top.
CalendarItemTitle = $"{Translator.CalendarItemAllDay} {CalendarItem.Title}";
}
else
{
// Not expected, but there it is.
CalendarItemTitle = CalendarItem.Title;
}
// Debug.WriteLine($"{CalendarItem.Title} Period relation with {DisplayingDate.Period.ToString()}: {periodRelation}");
}
else
{
CalendarItemTitle = CalendarItem.Title;
}
UpdateVisualStates();
}
private void UpdateVisualStates()
{
if (CalendarItem == null) return;
if (CalendarItem.IsAllDayEvent)
{
VisualStateManager.GoToState(this, "AllDayEvent", true);
}
else if (CalendarItem.IsMultiDayEvent)
{
if (IsCustomEventArea)
{
VisualStateManager.GoToState(this, "CustomAreaMultiDayEvent", true);
}
else
{
// Hide it.
VisualStateManager.GoToState(this, "MultiDayEvent", true);
}
}
else
{
VisualStateManager.GoToState(this, "RegularEvent", true);
}
}
private void ControlDragStarting(UIElement sender, DragStartingEventArgs args) => IsDragging = true;
private void ControlDropped(UIElement sender, DropCompletedEventArgs args) => IsDragging = false;
private async void ControlTapped(object sender, TappedRoutedEventArgs e)
{
if (CalendarItem == null) return;
isSingleTap = true;
await Task.Delay(100);
if (isSingleTap)
{
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, DisplayingDate));
}
}
private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
if (CalendarItem == null) return;
isSingleTap = false;
WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem));
}
private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e)
{
if (CalendarItem == null) return;
WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem));
}
}

View File

@@ -0,0 +1,42 @@
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls;
namespace Wino.Calendar.Controls;
/// <summary>
/// FlipView that hides the navigation buttons and exposes methods to navigate to the next and previous items with animations.
/// </summary>
public partial class CustomCalendarFlipView : FlipView
{
private const string PART_PreviousButton = "PreviousButtonHorizontal";
private const string PART_NextButton = "NextButtonHorizontal";
private Button PreviousButton;
private Button NextButton;
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
PreviousButton = GetTemplateChild(PART_PreviousButton) as Button;
NextButton = GetTemplateChild(PART_NextButton) as Button;
// Hide navigation buttons
PreviousButton.Opacity = NextButton.Opacity = 0;
PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false;
var t = FindName("ScrollingHost");
}
public void GoPreviousFlip()
{
var backPeer = new ButtonAutomationPeer(PreviousButton);
backPeer.Invoke();
}
public void GoNextFlip()
{
var nextPeer = new ButtonAutomationPeer(NextButton);
nextPeer.Invoke();
}
}

View File

@@ -0,0 +1,77 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
public partial class DayColumnControl : Control
{
private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText);
private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl);
private const string TodayState = nameof(TodayState);
private const string NotTodayState = nameof(NotTodayState);
private TextBlock HeaderDateDayText;
private TextBlock ColumnHeaderText;
private Border IsTodayBorder;
private ItemsControl AllDayItemsControl;
public CalendarDayModel DayModel
{
get { return (CalendarDayModel)GetValue(DayModelProperty); }
set { SetValue(DayModelProperty, value); }
}
public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public DayColumnControl()
{
DefaultStyleKey = typeof(DayColumnControl);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl;
UpdateValues();
}
private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
{
if (control is DayColumnControl columnControl)
{
columnControl.UpdateValues();
}
}
private void UpdateValues()
{
if (HeaderDateDayText == null || IsTodayBorder == null || DayModel == null) return;
HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
// Monthly template does not use it.
if (ColumnHeaderText != null)
{
ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
}
AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents;
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
UpdateLayout();
}
}

View File

@@ -0,0 +1,56 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
namespace Wino.Calendar.Controls;
public partial class DayHeaderControl : Control
{
private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock);
private TextBlock HeaderTextblock;
public DayHeaderDisplayType DisplayType
{
get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); }
set { SetValue(DisplayTypeProperty, value); }
}
public DateTime Date
{
get { return (DateTime)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty DateProperty = DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(DayHeaderControl), new PropertyMetadata(default(DateTime), new PropertyChangedCallback(OnHeaderPropertyChanged)));
public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(DayHeaderDisplayType), typeof(DayHeaderControl), new PropertyMetadata(DayHeaderDisplayType.TwentyFourHour, new PropertyChangedCallback(OnHeaderPropertyChanged)));
public DayHeaderControl()
{
DefaultStyleKey = typeof(DayHeaderControl);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
HeaderTextblock = GetTemplateChild(PART_DayHeaderTextBlock) as TextBlock;
UpdateHeaderText();
}
private static void OnHeaderPropertyChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
{
if (control is DayHeaderControl headerControl)
{
headerControl.UpdateHeaderText();
}
}
private void UpdateHeaderText()
{
if (HeaderTextblock != null)
{
HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm");
}
}
}

View File

@@ -0,0 +1,299 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Calendar.Args;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
using Wino.Helpers;
namespace Wino.Calendar.Controls;
public partial class WinoCalendarControl : Control
{
private const string PART_WinoFlipView = nameof(PART_WinoFlipView);
private const string PART_IdleGrid = nameof(PART_IdleGrid);
public event EventHandler<TimelineCellSelectedArgs> TimelineCellSelected;
public event EventHandler<TimelineCellUnselectedArgs> TimelineCellUnselected;
public event EventHandler ScrollPositionChanging;
#region Dependency Properties
public static readonly DependencyProperty DayRangesProperty = DependencyProperty.Register(nameof(DayRanges), typeof(ObservableCollection<DayRangeRenderModel>), typeof(WinoCalendarControl), new PropertyMetadata(null));
public static readonly DependencyProperty SelectedFlipViewIndexProperty = DependencyProperty.Register(nameof(SelectedFlipViewIndex), typeof(int), typeof(WinoCalendarControl), new PropertyMetadata(-1));
public static readonly DependencyProperty SelectedFlipViewDayRangeProperty = DependencyProperty.Register(nameof(SelectedFlipViewDayRange), typeof(DayRangeRenderModel), typeof(WinoCalendarControl), new PropertyMetadata(null));
public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveCanvasChanged)));
public static readonly DependencyProperty IsFlipIdleProperty = DependencyProperty.Register(nameof(IsFlipIdle), typeof(bool), typeof(WinoCalendarControl), new PropertyMetadata(true, new PropertyChangedCallback(OnIdleStateChanged)));
public static readonly DependencyProperty ActiveScrollViewerProperty = DependencyProperty.Register(nameof(ActiveScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveVerticalScrollViewerChanged)));
public static readonly DependencyProperty VerticalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(VerticalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
public static readonly DependencyProperty HorizontalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(HorizontalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(CalendarOrientation), typeof(WinoCalendarControl), new PropertyMetadata(CalendarOrientation.Horizontal, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(CalendarDisplayType), typeof(WinoCalendarControl), new PropertyMetadata(CalendarDisplayType.Day));
/// <summary>
/// Gets or sets the day-week-month-year display type.
/// Orientation is not determined by this property, but Orientation property.
/// This property is used to determine the template to use for the calendar.
/// </summary>
public CalendarDisplayType DisplayType
{
get { return (CalendarDisplayType)GetValue(DisplayTypeProperty); }
set { SetValue(DisplayTypeProperty, value); }
}
public CalendarOrientation Orientation
{
get { return (CalendarOrientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public ItemsPanelTemplate VerticalItemsPanelTemplate
{
get { return (ItemsPanelTemplate)GetValue(VerticalItemsPanelTemplateProperty); }
set { SetValue(VerticalItemsPanelTemplateProperty, value); }
}
public ItemsPanelTemplate HorizontalItemsPanelTemplate
{
get { return (ItemsPanelTemplate)GetValue(HorizontalItemsPanelTemplateProperty); }
set { SetValue(HorizontalItemsPanelTemplateProperty, value); }
}
public DayRangeRenderModel SelectedFlipViewDayRange
{
get { return (DayRangeRenderModel)GetValue(SelectedFlipViewDayRangeProperty); }
set { SetValue(SelectedFlipViewDayRangeProperty, value); }
}
public ScrollViewer ActiveScrollViewer
{
get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); }
set { SetValue(ActiveScrollViewerProperty, value); }
}
public WinoDayTimelineCanvas ActiveCanvas
{
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
set { SetValue(ActiveCanvasProperty, value); }
}
public bool IsFlipIdle
{
get { return (bool)GetValue(IsFlipIdleProperty); }
set { SetValue(IsFlipIdleProperty, value); }
}
/// <summary>
/// Gets or sets the collection of day ranges to render.
/// Each day range usually represents a week, but it may support other ranges.
/// </summary>
public ObservableCollection<DayRangeRenderModel> DayRanges
{
get { return (ObservableCollection<DayRangeRenderModel>)GetValue(DayRangesProperty); }
set { SetValue(DayRangesProperty, value); }
}
public int SelectedFlipViewIndex
{
get { return (int)GetValue(SelectedFlipViewIndexProperty); }
set { SetValue(SelectedFlipViewIndexProperty, value); }
}
#endregion
private WinoCalendarFlipView InternalFlipView;
private Grid IdleGrid;
public WinoCalendarControl()
{
DefaultStyleKey = typeof(WinoCalendarControl);
SizeChanged += CalendarSizeChanged;
}
private static void OnCalendarOrientationPropertiesUpdated(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl control)
{
control.ManageCalendarOrientation();
}
}
private static void OnIdleStateChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl calendarControl)
{
calendarControl.UpdateIdleState();
}
}
private static void OnActiveVerticalScrollViewerChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl calendarControl)
{
if (e.OldValue is ScrollViewer oldScrollViewer)
{
calendarControl.DeregisterScrollChanges(oldScrollViewer);
}
if (e.NewValue is ScrollViewer newScrollViewer)
{
calendarControl.RegisterScrollChanges(newScrollViewer);
}
calendarControl.ManageHighlightedDateRange();
}
}
private static void OnActiveCanvasChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl calendarControl)
{
if (e.OldValue is WinoDayTimelineCanvas oldCanvas)
{
// Dismiss any selection on the old canvas.
calendarControl.DeregisterCanvas(oldCanvas);
}
if (e.NewValue is WinoDayTimelineCanvas newCanvas)
{
calendarControl.RegisterCanvas(newCanvas);
}
calendarControl.ManageHighlightedDateRange();
}
}
private void ManageCalendarOrientation()
{
if (InternalFlipView == null || HorizontalItemsPanelTemplate == null || VerticalItemsPanelTemplate == null) return;
InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate;
}
private void ManageHighlightedDateRange()
=> SelectedFlipViewDayRange = InternalFlipView.SelectedItem as DayRangeRenderModel;
private void DeregisterCanvas(WinoDayTimelineCanvas canvas)
{
if (canvas == null) return;
canvas.SelectedDateTime = null;
canvas.TimelineCellSelected -= ActiveTimelineCellSelected;
canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected;
}
private void RegisterCanvas(WinoDayTimelineCanvas canvas)
{
if (canvas == null) return;
canvas.SelectedDateTime = null;
canvas.TimelineCellSelected += ActiveTimelineCellSelected;
canvas.TimelineCellUnselected += ActiveTimelineCellUnselected;
}
private void RegisterScrollChanges(ScrollViewer scrollViewer)
{
if (scrollViewer == null) return;
scrollViewer.ViewChanging += ScrollViewChanging;
}
private void DeregisterScrollChanges(ScrollViewer scrollViewer)
{
if (scrollViewer == null) return;
scrollViewer.ViewChanging -= ScrollViewChanging;
}
private void ScrollViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
=> ScrollPositionChanging?.Invoke(this, EventArgs.Empty);
private void CalendarSizeChanged(object sender, SizeChangedEventArgs e)
{
if (ActiveCanvas == null) return;
ActiveCanvas.SelectedDateTime = null;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView;
IdleGrid = GetTemplateChild(PART_IdleGrid) as Grid;
UpdateIdleState();
ManageCalendarOrientation();
}
private void UpdateIdleState()
{
InternalFlipView.Opacity = IsFlipIdle ? 0 : 1;
IdleGrid.Visibility = IsFlipIdle ? Visibility.Visible : Visibility.Collapsed;
}
private void ActiveTimelineCellUnselected(object sender, TimelineCellUnselectedArgs e)
=> TimelineCellUnselected?.Invoke(this, e);
private void ActiveTimelineCellSelected(object sender, TimelineCellSelectedArgs e)
=> TimelineCellSelected?.Invoke(this, e);
public void NavigateToDay(DateTime dateTime) => InternalFlipView.NavigateToDay(dateTime);
public async void NavigateToHour(TimeSpan timeSpan)
{
if (ActiveScrollViewer == null) return;
// Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers.
await Task.Yield();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
{
double hourHeght = 60;
double totalHeight = ActiveScrollViewer.ScrollableHeight;
double scrollPosition = timeSpan.TotalHours * hourHeght;
ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false);
});
}
public void ResetTimelineSelection()
{
if (ActiveCanvas == null) return;
ActiveCanvas.SelectedDateTime = null;
}
public void GoNextRange()
{
if (InternalFlipView == null) return;
InternalFlipView.GoNextFlip();
}
public void GoPreviousRange()
{
if (InternalFlipView == null) return;
InternalFlipView.GoPreviousFlip();
}
public void UnselectActiveTimelineCell()
{
if (ActiveCanvas == null) return;
ActiveCanvas.SelectedDateTime = null;
}
public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel)
{
return this.FindDescendants<CalendarItemControl>().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel);
}
}

View File

@@ -0,0 +1,185 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
public partial class WinoCalendarFlipView : CustomCalendarFlipView
{
public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true));
public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
public static readonly DependencyProperty ActiveVerticalScrollViewerProperty = DependencyProperty.Register(nameof(ActiveVerticalScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
/// <summary>
/// Gets or sets the active canvas that is currently displayed in the flip view.
/// Each day-range of flip view item has a canvas that displays the day timeline.
/// </summary>
public WinoDayTimelineCanvas ActiveCanvas
{
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
set { SetValue(ActiveCanvasProperty, value); }
}
/// <summary>
/// Gets or sets the scroll viewer that is currently active in the flip view.
/// It's the vertical scroll that scrolls the timeline only, not the header part that belongs
/// to parent FlipView control.
/// </summary>
public ScrollViewer ActiveVerticalScrollViewer
{
get { return (ScrollViewer)GetValue(ActiveVerticalScrollViewerProperty); }
set { SetValue(ActiveVerticalScrollViewerProperty, value); }
}
public bool IsIdle
{
get { return (bool)GetValue(IsIdleProperty); }
set { SetValue(IsIdleProperty, value); }
}
public WinoCalendarFlipView()
{
RegisterPropertyChangedCallback(SelectedIndexProperty, new DependencyPropertyChangedCallback(OnSelectedIndexUpdated));
RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged));
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyProperty e)
{
if (d is WinoCalendarFlipView flipView)
{
flipView.RegisterItemsSourceChange();
}
}
private static void OnSelectedIndexUpdated(DependencyObject d, DependencyProperty e)
{
if (d is WinoCalendarFlipView flipView)
{
flipView.UpdateActiveCanvas();
flipView.UpdateActiveScrollViewer();
}
}
private void RegisterItemsSourceChange()
{
if (GetItemsSource() is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += ItemsSourceUpdated;
}
}
private void ItemsSourceUpdated(object sender, NotifyCollectionChangedEventArgs e)
{
IsIdle = e.Action == NotifyCollectionChangedAction.Reset || e.Action == NotifyCollectionChangedAction.Replace;
}
private async Task<FlipViewItem> GetCurrentFlipViewItem()
{
// TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together.
while (ContainerFromIndex(SelectedIndex) == null)
{
await Task.Delay(100);
}
return ContainerFromIndex(SelectedIndex) as FlipViewItem;
}
private void UpdateActiveScrollViewer()
{
if (SelectedIndex < 0)
ActiveVerticalScrollViewer = null;
else
{
GetCurrentFlipViewItem().ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
{
var flipViewItem = task.Result;
_ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ActiveVerticalScrollViewer = flipViewItem.FindDescendant<ScrollViewer>();
});
}
});
}
}
public void UpdateActiveCanvas()
{
if (SelectedIndex < 0)
ActiveCanvas = null;
else
{
GetCurrentFlipViewItem().ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
{
var flipViewItem = task.Result;
_ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ActiveCanvas = flipViewItem.FindDescendant<WinoDayTimelineCanvas>();
});
}
});
}
}
/// <summary>
/// Navigates to the specified date in the calendar.
/// </summary>
/// <param name="dateTime">Date to navigate.</param>
public async void NavigateToDay(DateTime dateTime)
{
await Task.Yield();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
{
// Find the day range that contains the date.
var dayRange = GetItemsSource()?.FirstOrDefault(a => a.CalendarDays.Any(b => b.RepresentingDate.Date == dateTime.Date));
if (dayRange != null)
{
var navigationItemIndex = GetItemsSource().IndexOf(dayRange);
if (Math.Abs(navigationItemIndex - SelectedIndex) > 4)
{
// Difference between dates are high.
// No need to animate this much, just go without animating.
SelectedIndex = navigationItemIndex;
}
else
{
// Until we reach the day in the flip, simulate next-prev button clicks.
// This will make sure the FlipView animations are triggered.
// Setting SelectedIndex directly doesn't trigger the animations.
while (SelectedIndex != navigationItemIndex)
{
if (SelectedIndex > navigationItemIndex)
{
GoPreviousFlip();
}
else
{
GoNextFlip();
}
}
}
}
});
}
private ObservableRangeCollection<DayRangeRenderModel> GetItemsSource()
=> ItemsSource as ObservableRangeCollection<DayRangeRenderModel>;
}

View File

@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.WinUI;
using Itenso.TimePeriod;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Calendar.Models;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.Controls;
public partial class WinoCalendarPanel : Panel
{
private const double LastItemRightExtraMargin = 12d;
// Store each ICalendarItem measurements by their Id.
private readonly Dictionary<ICalendarItem, CalendarItemMeasurement> _measurements = new Dictionary<ICalendarItem, CalendarItemMeasurement>();
public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0)));
public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d));
public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
public ITimePeriod Period
{
get { return (ITimePeriod)GetValue(PeriodProperty); }
set { SetValue(PeriodProperty, value); }
}
public double HourHeight
{
get { return (double)GetValue(HourHeightProperty); }
set { SetValue(HourHeightProperty, value); }
}
public Thickness EventItemMargin
{
get { return (Thickness)GetValue(EventItemMarginProperty); }
set { SetValue(EventItemMarginProperty, value); }
}
private void ResetMeasurements() => _measurements.Clear();
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
{
var childStart = calendarItemViewModel.StartDate;
if (childStart <= Period.Start)
{
// Event started before or exactly at the periods tart. This might be a multi-day event.
// We can simply consider event must not have a top margin.
return 0d;
}
double minutesFromStart = (childStart - Period.Start).TotalMinutes;
return (minutesFromStart / 1440) * availableHeight;
}
private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
{
return (calendarItemMeasurement.Right - calendarItemMeasurement.Left) * availableWidth;
}
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
=> availableWidth * calendarItemMeasurement.Left;
private double GetChildHeight(ICalendarItem child)
{
// All day events are not measured.
if (child.IsAllDayEvent) return 0;
double childDurationInMinutes = 0d;
double availableHeight = HourHeight * 24;
var periodRelation = child.Period.GetRelation(Period);
// Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
if (!child.IsMultiDayEvent)
{
childDurationInMinutes = child.Period.Duration.TotalMinutes;
}
else
{
// Multi-day event.
// Check how many of the event falls into the current period.
childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
}
return (childDurationInMinutes / 1440) * availableHeight;
}
protected override Size MeasureOverride(Size availableSize)
{
ResetMeasurements();
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Period == null || HourHeight == 0d) return finalSize;
// Measure/arrange each child height and width.
// This is a vertical calendar. Therefore the height of each child is the duration of the event.
// Children weights for left and right will be saved if they don't exist.
// This is important because we don't want to measure the weights again.
// They don't change until new event is added or removed.
// Width of the each child may depend on the rectangle packing algorithm.
// Children are first categorized into columns. Then each column is shifted to the left until
// no overlap occurs. The width of each child is calculated based on the number of columns it spans.
double availableHeight = finalSize.Height;
double availableWidth = finalSize.Width;
var calendarControls = Children.Cast<ContentPresenter>();
if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
var events = calendarControls.Select(a => a.Content as CalendarItemViewModel);
LayoutEvents(events);
foreach (var control in calendarControls)
{
// We can't arrange this child.
if (!(control.Content is ICalendarItem child)) continue;
bool isHorizontallyLastItem = false;
double childWidth = 0,
childHeight = Math.Max(0, GetChildHeight(child)),
childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)),
childLeft = 0;
// No need to measure anything here.
if (childHeight == 0) continue;
if (!_measurements.ContainsKey(child))
{
// Multi-day event.
childLeft = 0;
childWidth = availableWidth;
}
else
{
var childMeasurement = _measurements[child];
childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
isHorizontallyLastItem = childMeasurement.Right == 1;
}
// Add additional right margin to items that falls on the right edge of the panel.
double extraRightMargin = 0;
// Multi-day events don't have any margin and their hit test is disabled.
if (!child.IsMultiDayEvent)
{
// Max of 5% of the width or 20px max.
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
}
if (childWidth < 0) childWidth = 1;
// Regular events must have 2px margin
if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
{
childLeft += 2;
childTop += 2;
childHeight -= 2;
childWidth -= 2;
}
var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight);
// Make sure measured size will fit in the arranged box.
var measureSize = arrangementRect.ToSize();
control.Measure(measureSize);
control.Arrange(arrangementRect);
//Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}");
}
return finalSize;
}
#region ColumSpanning and Packing Algorithm
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
{
if (_measurements.ContainsKey(calendarItem))
{
_measurements[calendarItem] = measurement;
}
else
{
_measurements.Add(calendarItem, measurement);
}
}
// Pick the left and right positions of each event, such that there are no overlap.
private void LayoutEvents(IEnumerable<ICalendarItem> events)
{
var columns = new List<List<ICalendarItem>>();
DateTime? lastEventEnding = null;
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
{
// Multi-day events are not measured.
if (ev.IsMultiDayEvent) continue;
if (ev.Period.Start >= lastEventEnding)
{
PackEvents(columns);
columns.Clear();
lastEventEnding = null;
}
bool placed = false;
foreach (var col in columns)
{
if (!col.Last().Period.OverlapsWith(ev.Period))
{
col.Add(ev);
placed = true;
break;
}
}
if (!placed)
{
columns.Add(new List<ICalendarItem> { ev });
}
if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value)
{
lastEventEnding = ev.Period.End;
}
}
if (columns.Count > 0)
{
PackEvents(columns);
}
}
// Set the left and right positions for each event in the connected group.
private void PackEvents(List<List<ICalendarItem>> columns)
{
float numColumns = columns.Count;
int iColumn = 0;
foreach (var col in columns)
{
foreach (var ev in col)
{
int colSpan = ExpandEvent(ev, iColumn, columns);
var leftWeight = iColumn / numColumns;
var rightWeight = (iColumn + colSpan) / numColumns;
AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
}
iColumn++;
}
}
// Checks how many columns the event can expand into, without colliding with other events.
private int ExpandEvent(ICalendarItem ev, int iColumn, List<List<ICalendarItem>> columns)
{
int colSpan = 1;
foreach (var col in columns.Skip(iColumn + 1))
{
foreach (var ev1 in col)
{
if (ev1.Period.OverlapsWith(ev.Period)) return colSpan;
}
colSpan++;
}
return colSpan;
}
#endregion
}

View File

@@ -0,0 +1,91 @@
using System.Windows.Input;
using CommunityToolkit.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
namespace Wino.Calendar.Controls;
public partial class WinoCalendarTypeSelectorControl : Control
{
private const string PART_TodayButton = nameof(PART_TodayButton);
private const string PART_DayToggle = nameof(PART_DayToggle);
private const string PART_WeekToggle = nameof(PART_WeekToggle);
private const string PART_MonthToggle = nameof(PART_MonthToggle);
private const string PART_YearToggle = nameof(PART_YearToggle);
public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(nameof(SelectedType), typeof(CalendarDisplayType), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(CalendarDisplayType.Week));
public static readonly DependencyProperty DisplayDayCountProperty = DependencyProperty.Register(nameof(DisplayDayCount), typeof(int), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(0));
public static readonly DependencyProperty TodayClickedCommandProperty = DependencyProperty.Register(nameof(TodayClickedCommand), typeof(ICommand), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(null));
public ICommand TodayClickedCommand
{
get { return (ICommand)GetValue(TodayClickedCommandProperty); }
set { SetValue(TodayClickedCommandProperty, value); }
}
public CalendarDisplayType SelectedType
{
get { return (CalendarDisplayType)GetValue(SelectedTypeProperty); }
set { SetValue(SelectedTypeProperty, value); }
}
public int DisplayDayCount
{
get { return (int)GetValue(DisplayDayCountProperty); }
set { SetValue(DisplayDayCountProperty, value); }
}
private AppBarButton _todayButton;
private AppBarToggleButton _dayToggle;
private AppBarToggleButton _weekToggle;
private AppBarToggleButton _monthToggle;
private AppBarToggleButton _yearToggle;
public WinoCalendarTypeSelectorControl()
{
DefaultStyleKey = typeof(WinoCalendarTypeSelectorControl);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton;
_dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton;
_weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton;
_monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton;
_yearToggle = GetTemplateChild(PART_YearToggle) as AppBarToggleButton;
Guard.IsNotNull(_todayButton, nameof(_todayButton));
Guard.IsNotNull(_dayToggle, nameof(_dayToggle));
Guard.IsNotNull(_weekToggle, nameof(_weekToggle));
Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
Guard.IsNotNull(_yearToggle, nameof(_yearToggle));
_todayButton.Click += TodayClicked;
_dayToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); };
_weekToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); };
_monthToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); };
_yearToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Year); };
UpdateToggleButtonStates();
}
private void TodayClicked(object sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null);
private void SetSelectedType(CalendarDisplayType type)
{
SelectedType = type;
UpdateToggleButtonStates();
}
private void UpdateToggleButtonStates()
{
_dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day;
_weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week;
_monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month;
_yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year;
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Windows.Input;
using CommunityToolkit.Diagnostics;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Wino.Core.Domain.Models.Calendar;
using Wino.Helpers;
namespace Wino.Calendar.Controls;
public partial class WinoCalendarView : Control
{
private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder);
private const string PART_CalendarView = nameof(PART_CalendarView);
public static readonly DependencyProperty HighlightedDateRangeProperty = DependencyProperty.Register(nameof(HighlightedDateRange), typeof(DateRange), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnHighlightedDateRangeChanged)));
public static readonly DependencyProperty VisibleDateBackgroundProperty = DependencyProperty.Register(nameof(VisibleDateBackground), typeof(Brush), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnPropertiesChanged)));
public static readonly DependencyProperty DateClickedCommandProperty = DependencyProperty.Register(nameof(DateClickedCommand), typeof(ICommand), typeof(WinoCalendarView), new PropertyMetadata(null));
public static readonly DependencyProperty TodayBackgroundColorProperty = DependencyProperty.Register(nameof(TodayBackgroundColor), typeof(Color), typeof(WinoCalendarView), new PropertyMetadata(null));
public Color TodayBackgroundColor
{
get { return (Color)GetValue(TodayBackgroundColorProperty); }
set { SetValue(TodayBackgroundColorProperty, value); }
}
/// <summary>
/// Gets or sets the command to execute when a date is picked.
/// Unused.
/// </summary>
public ICommand DateClickedCommand
{
get { return (ICommand)GetValue(DateClickedCommandProperty); }
set { SetValue(DateClickedCommandProperty, value); }
}
/// <summary>
/// Gets or sets the highlighted range of dates.
/// </summary>
public DateRange HighlightedDateRange
{
get { return (DateRange)GetValue(HighlightedDateRangeProperty); }
set { SetValue(HighlightedDateRangeProperty, value); }
}
public Brush VisibleDateBackground
{
get { return (Brush)GetValue(VisibleDateBackgroundProperty); }
set { SetValue(VisibleDateBackgroundProperty, value); }
}
private CalendarView CalendarView;
public WinoCalendarView()
{
DefaultStyleKey = typeof(WinoCalendarView);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
CalendarView = GetTemplateChild(PART_CalendarView) as CalendarView;
Guard.IsNotNull(CalendarView, nameof(CalendarView));
CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
CalendarView.SelectedDatesChanged += InternalCalendarViewSelectionChanged;
// TODO: Should come from settings.
CalendarView.FirstDayOfWeek = Windows.Globalization.DayOfWeek.Monday;
// Everytime display mode changes, update the visible date range backgrounds.
// If users go back from year -> month -> day, we need to update the visible date range backgrounds.
CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds());
}
private void InternalCalendarViewSelectionChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
{
if (args.AddedDates?.Count > 0)
{
var clickedDate = args.AddedDates[0].Date;
SetInnerDisplayDate(clickedDate);
var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate);
DateClickedCommand?.Execute(clickArgs);
}
// Reset selection, we don't show selected dates but react to them.
CalendarView.SelectedDates.Clear();
}
private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinoCalendarView control)
{
control.UpdateVisibleDateRangeBackgrounds();
}
}
private void SetInnerDisplayDate(DateTime dateTime) => CalendarView?.SetDisplayDate(dateTime);
// Changing selected dates will trigger the selection changed event.
// It will behave like user clicked the date.
public void GoToDay(DateTime dateTime) => CalendarView.SelectedDates.Add(dateTime);
private static void OnHighlightedDateRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinoCalendarView control)
{
control.SetInnerDisplayDate(control.HighlightedDateRange.StartDate);
control.UpdateVisibleDateRangeBackgrounds();
}
}
public void UpdateVisibleDateRangeBackgrounds()
{
if (HighlightedDateRange == null || VisibleDateBackground == null || TodayBackgroundColor == null || CalendarView == null) return;
var markDateCalendarDayItems = WinoVisualTreeHelper.FindDescendants<CalendarViewDayItem>(CalendarView);
foreach (var calendarDayItem in markDateCalendarDayItems)
{
var border = WinoVisualTreeHelper.GetChildObject<Border>(calendarDayItem, PART_DayViewItemBorder);
if (border == null) return;
if (calendarDayItem.Date.Date == DateTime.Today.Date)
{
border.Background = new SolidColorBrush(TodayBackgroundColor);
}
else if (calendarDayItem.Date.Date >= HighlightedDateRange.StartDate.Date && calendarDayItem.Date.Date < HighlightedDateRange.EndDate.Date)
{
border.Background = VisibleDateBackground;
}
else
{
border.Background = null;
}
}
}
}

View File

@@ -0,0 +1,277 @@
using System;
using System.Diagnostics;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Windows.Foundation;
using Windows.UI.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Wino.Calendar.Args;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
public partial class WinoDayTimelineCanvas : Control, IDisposable
{
public event EventHandler<TimelineCellSelectedArgs> TimelineCellSelected;
public event EventHandler<TimelineCellUnselectedArgs> TimelineCellUnselected;
private const string PART_InternalCanvas = nameof(PART_InternalCanvas);
private CanvasControl Canvas;
public static readonly DependencyProperty RenderOptionsProperty = DependencyProperty.Register(nameof(RenderOptions), typeof(CalendarRenderOptions), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public static readonly DependencyProperty SeperatorColorProperty = DependencyProperty.Register(nameof(SeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public static readonly DependencyProperty HalfHourSeperatorColorProperty = DependencyProperty.Register(nameof(HalfHourSeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public static readonly DependencyProperty SelectedCellBackgroundBrushProperty = DependencyProperty.Register(nameof(SelectedCellBackgroundBrush), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public static readonly DependencyProperty WorkingHourCellBackgroundColorProperty = DependencyProperty.Register(nameof(WorkingHourCellBackgroundColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public static readonly DependencyProperty SelectedDateTimeProperty = DependencyProperty.Register(nameof(SelectedDateTime), typeof(DateTime?), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedDateTimeChanged)));
public static readonly DependencyProperty PositionerUIElementProperty = DependencyProperty.Register(nameof(PositionerUIElement), typeof(UIElement), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null));
public UIElement PositionerUIElement
{
get { return (UIElement)GetValue(PositionerUIElementProperty); }
set { SetValue(PositionerUIElementProperty, value); }
}
public CalendarRenderOptions RenderOptions
{
get { return (CalendarRenderOptions)GetValue(RenderOptionsProperty); }
set { SetValue(RenderOptionsProperty, value); }
}
public SolidColorBrush HalfHourSeperatorColor
{
get { return (SolidColorBrush)GetValue(HalfHourSeperatorColorProperty); }
set { SetValue(HalfHourSeperatorColorProperty, value); }
}
public SolidColorBrush SeperatorColor
{
get { return (SolidColorBrush)GetValue(SeperatorColorProperty); }
set { SetValue(SeperatorColorProperty, value); }
}
public SolidColorBrush WorkingHourCellBackgroundColor
{
get { return (SolidColorBrush)GetValue(WorkingHourCellBackgroundColorProperty); }
set { SetValue(WorkingHourCellBackgroundColorProperty, value); }
}
public SolidColorBrush SelectedCellBackgroundBrush
{
get { return (SolidColorBrush)GetValue(SelectedCellBackgroundBrushProperty); }
set { SetValue(SelectedCellBackgroundBrushProperty, value); }
}
public DateTime? SelectedDateTime
{
get { return (DateTime?)GetValue(SelectedDateTimeProperty); }
set { SetValue(SelectedDateTimeProperty, value); }
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
Canvas = GetTemplateChild(PART_InternalCanvas) as CanvasControl;
// TODO: These will leak. Dispose them properly when needed.
Canvas.Draw += OnCanvasDraw;
Canvas.PointerPressed += OnCanvasPointerPressed;
ForceDraw();
}
private static void OnSelectedDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinoDayTimelineCanvas control)
{
if (e.OldValue != null && e.NewValue == null)
{
control.RaiseCellUnselected();
}
control.ForceDraw();
}
}
private void RaiseCellUnselected()
{
TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs());
}
private void OnCanvasPointerPressed(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (RenderOptions == null) return;
var hourHeight = RenderOptions.CalendarSettings.HourHeight;
// When users click to cell we need to find the day, hour and minutes (first 30 minutes or second 30 minutes) that it represents on the timeline.
PointerPoint positionerRootPoint = e.GetCurrentPoint(PositionerUIElement);
PointerPoint canvasPointerPoint = e.GetCurrentPoint(Canvas);
Point touchPoint = canvasPointerPoint.Position;
var singleDayWidth = (Canvas.ActualWidth / RenderOptions.TotalDayCount);
int day = (int)(touchPoint.X / singleDayWidth);
int hour = (int)(touchPoint.Y / hourHeight);
bool isSecondHalf = touchPoint.Y % hourHeight > (hourHeight / 2);
var diffX = positionerRootPoint.Position.X - touchPoint.X;
var diffY = positionerRootPoint.Position.Y - touchPoint.Y;
var cellStartRelativePositionX = diffX + (day * singleDayWidth);
var cellEndRelativePositionX = cellStartRelativePositionX + singleDayWidth;
var cellStartRelativePositionY = diffY + (hour * hourHeight) + (isSecondHalf ? hourHeight / 2 : 0);
var cellEndRelativePositionY = cellStartRelativePositionY + (isSecondHalf ? (hourHeight / 2) : hourHeight);
var cellSize = new Size(cellEndRelativePositionX - cellStartRelativePositionX, hourHeight / 2);
var positionerPoint = new Point(cellStartRelativePositionX, cellStartRelativePositionY);
var clickedDateTime = RenderOptions.DateRange.StartDate.AddDays(day).AddHours(hour).AddMinutes(isSecondHalf ? 30 : 0);
// If there is already a selected date, in order to mimic the popup behavior, we need to dismiss the previous selection first.
// Next click will be a new selection.
// Raise the events directly here instead of DP to not lose pointer position.
if (clickedDateTime == SelectedDateTime || SelectedDateTime != null)
{
SelectedDateTime = null;
}
else
{
SelectedDateTime = clickedDateTime;
TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize));
}
Debug.WriteLine($"Clicked: {clickedDateTime}");
}
public WinoDayTimelineCanvas()
{
DefaultStyleKey = typeof(WinoDayTimelineCanvas);
}
private static void OnRenderingPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinoDayTimelineCanvas control)
{
control.ForceDraw();
}
}
private void ForceDraw() => Canvas?.Invalidate();
private bool CanDrawTimeline()
{
return RenderOptions != null
&& Canvas != null
&& Canvas.ReadyToDraw
&& WorkingHourCellBackgroundColor != null
&& SeperatorColor != null
&& HalfHourSeperatorColor != null
&& SelectedCellBackgroundBrush != null;
}
private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args)
{
if (!CanDrawTimeline()) return;
int hours = 24;
double canvasWidth = Canvas.ActualWidth;
double canvasHeight = Canvas.ActualHeight;
if (canvasWidth == 0 || canvasHeight == 0) return;
// Calculate the width of each rectangle (1 day column)
// Equal distribution of the whole width.
double rectWidth = canvasWidth / RenderOptions.TotalDayCount;
// Calculate the height of each rectangle (1 hour row)
double rectHeight = RenderOptions.CalendarSettings.HourHeight;
// Define stroke and fill colors
var strokeColor = SeperatorColor.Color;
float strokeThickness = 0.5f;
for (int day = 0; day < RenderOptions.TotalDayCount; day++)
{
var currentDay = RenderOptions.DateRange.StartDate.AddDays(day);
bool isWorkingDay = RenderOptions.CalendarSettings.WorkingDays.Contains(currentDay.DayOfWeek);
// Loop through each hour (rows)
for (int hour = 0; hour < hours; hour++)
{
var renderTime = TimeSpan.FromHours(hour);
var representingDateTime = currentDay.AddHours(hour);
// Calculate the position and size of the rectangle
double x = day * rectWidth;
double y = hour * rectHeight;
var rectangle = new Rect(x, y, rectWidth, rectHeight);
// Draw the rectangle border.
// This is the main rectangle.
args.DrawingSession.DrawRectangle(rectangle, strokeColor, strokeThickness);
// Fill another rectangle with the working hour background color
// This rectangle must be placed with -1 margin to prevent invisible borders of the main rectangle.
if (isWorkingDay && renderTime >= RenderOptions.CalendarSettings.WorkingHourStart && renderTime <= RenderOptions.CalendarSettings.WorkingHourEnd)
{
var backgroundRectangle = new Rect(x + 1, y + 1, rectWidth - 1, rectHeight - 1);
args.DrawingSession.DrawRectangle(backgroundRectangle, strokeColor, strokeThickness);
args.DrawingSession.FillRectangle(backgroundRectangle, WorkingHourCellBackgroundColor.Color);
}
// Draw a line in the center of the rectangle for representing half hours.
double lineY = y + rectHeight / 2;
args.DrawingSession.DrawLine((float)x, (float)lineY, (float)(x + rectWidth), (float)lineY, HalfHourSeperatorColor.Color, strokeThickness, new CanvasStrokeStyle()
{
DashStyle = CanvasDashStyle.Dot
});
}
// Draw selected item background color for the date if possible.
if (SelectedDateTime != null)
{
var selectedDateTime = SelectedDateTime.Value;
if (selectedDateTime.Date == currentDay.Date)
{
var selectionRectHeight = rectHeight / 2;
var selectedY = selectedDateTime.Hour * rectHeight + (selectedDateTime.Minute / 60) * rectHeight;
// Second half of the hour is selected.
if (selectedDateTime.TimeOfDay.Minutes == 30)
{
selectedY += rectHeight / 2;
}
var selectedRectangle = new Rect(day * rectWidth, selectedY, rectWidth, selectionRectHeight);
args.DrawingSession.FillRectangle(selectedRectangle, SelectedCellBackgroundBrush.Color);
}
}
}
}
public void Dispose()
{
if (Canvas == null) return;
Canvas.Draw -= OnCanvasDraw;
Canvas.PointerPressed -= OnCanvasPointerPressed;
Canvas.RemoveFromVisualTree();
Canvas = null;
}
}

View File

@@ -0,0 +1,106 @@
using System.Linq;
using System.Text.RegularExpressions;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Windows.UI.Xaml.Controls.Primitives;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
using Wino.Helpers;
namespace Wino.Calendar.Helpers;
public static class CalendarXamlHelpers
{
public static CalendarItemViewModel GetFirstAllDayEvent(CalendarEventCollection collection)
=> (CalendarItemViewModel)collection.AllDayEvents.FirstOrDefault();
/// <summary>
/// Returns full date + duration info in Event Details page details title.
/// </summary>
public static string GetEventDetailsDateString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
if (calendarItemViewModel == null || settings == null) return string.Empty;
var start = calendarItemViewModel.Period.Start;
var end = calendarItemViewModel.Period.End;
string timeFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "h:mm tt" : "HH:mm";
string dateFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "dddd, dd MMMM h:mm tt" : "dddd, dd MMMM HH:mm";
if (calendarItemViewModel.IsMultiDayEvent)
{
return $"{start.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)} - {end.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)}";
}
else
{
return $"{start.ToString(dateFormat, settings.CultureInfo)} - {end.ToString(timeFormat, settings.CultureInfo)}";
}
}
public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel)
{
if (calendarItemViewModel == null || !calendarItemViewModel.IsRecurringChild) return string.Empty;
// Parse recurrence rules
var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(calendarItemViewModel.StartDate),
End = new CalDateTime(calendarItemViewModel.EndDate),
};
var recurrenceLines = Regex.Split(calendarItemViewModel.CalendarItem.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
if (calendarEvent.RecurrenceRules == null || !calendarEvent.RecurrenceRules.Any())
{
return "No recurrence pattern.";
}
var recurrenceRule = calendarEvent.RecurrenceRules.First();
var daysOfWeek = string.Join(", ", recurrenceRule.ByDay.Select(day => day.DayOfWeek.ToString()));
string timeZone = calendarEvent.DtStart.TzId ?? "UTC";
return $"Every {daysOfWeek}, effective {calendarEvent.DtStart.Value.ToShortDateString()} " +
$"from {calendarEvent.DtStart.Value.ToShortTimeString()} to {calendarEvent.DtEnd.Value.ToShortTimeString()} " +
$"{timeZone}.";
}
public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
if (calendarItemViewModel == null || settings == null) return string.Empty;
// Single event in a day.
if (!calendarItemViewModel.IsAllDayEvent && !calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} {settings.GetTimeString(calendarItemViewModel.Period.Duration)}";
}
else if (calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}";
}
else
{
// All day event.
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} ({Translator.CalendarItemAllDay})";
}
}
public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup(
CalendarItemViewModel calendarItemViewModel,
CalendarDisplayType calendarDisplayType)
{
if (calendarItemViewModel == null) return PopupPlacementMode.Auto;
// All and/or multi day events always go to the top of the screen.
if (calendarItemViewModel.IsAllDayEvent || calendarItemViewModel.IsMultiDayEvent) return PopupPlacementMode.Bottom;
return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType);
}
}

View File

@@ -2,13 +2,11 @@
x:Class="Wino.Calendar.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Wino.Calendar"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Wino.Calendar"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid>
</Grid>
<Grid />
</Page>

View File

@@ -15,16 +15,15 @@ using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace Wino.Calendar
namespace Wino.Calendar;
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
public MainPage()
{
public MainPage()
{
this.InitializeComponent();
}
this.InitializeComponent();
}
}

View File

@@ -0,0 +1,16 @@
namespace Wino.Calendar.Models;
public struct CalendarItemMeasurement
{
// Where to start?
public double Left { get; set; }
// Extend until where?
public double Right { get; set; }
public CalendarItemMeasurement(double left, double right)
{
Left = left;
Right = right;
}
}

View File

@@ -3,9 +3,16 @@
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
IgnorableNamespaces="uap mp">
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.15.0" />
<!-- Publisher Cache Folders -->
<Extensions>
<Extension Category="windows.publisherCacheFolders">
@@ -14,13 +21,8 @@
</PublisherCacheFolders>
</Extension>
</Extensions>
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="f047b7dd-96ec-4d54-a862-9321e271e449" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<mp:PhoneIdentity PhoneProductId="f047b7dd-96ec-4d54-a862-9321e271e449" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Wino Calendar</DisplayName>
@@ -46,13 +48,31 @@
Square44x44Logo="Assets\Square44x44Logo.png"
Description="Wino.Calendar"
BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<!-- Protocol activation: Google oAuth -->
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="google.pw.oauth2">
<uap:DisplayName>Google Auth Protocol</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
<uap5:Extension Category="windows.startupTask">
<uap5:StartupTask
TaskId="WinoStartupId"
Enabled="false"
DisplayName="Wino Startup Service" />
</uap5:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<Capability Name="internetClient" />
<Capability Name="privateNetworkClientServer"/>
<Capability Name="internetClientServer"/>
</Capabilities>
</Package>
</Package>

View File

@@ -1,29 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Wino.Calendar")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Wino.Calendar")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: ComVisible(false)]

View File

@@ -1,31 +0,0 @@
<!--
This file contains Runtime Directives used by .NET Native. The defaults here are suitable for most
developers. However, you can modify these parameters to modify the behavior of the .NET Native
optimizer.
Runtime Directives are documented at https://go.microsoft.com/fwlink/?LinkID=391919
To fully enable reflection for App1.MyClass and all of its public/private members
<Type Name="App1.MyClass" Dynamic="Required All"/>
To enable dynamic creation of the specific instantiation of AppClass<T> over System.Int32
<TypeInstantiation Name="App1.AppClass" Arguments="System.Int32" Activate="Required Public" />
Using the Namespace directive to apply reflection policy to all the types in a particular namespace
<Namespace Name="DataClasses.ViewModels" Serialize="All" />
-->
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<!--
An Assembly element with Name="*Application*" applies to all assemblies in
the application package. The asterisks are not wildcards.
-->
<Assembly Name="*Application*" Dynamic="Required All" />
<!-- Add your application specific runtime directives here. -->
</Application>
</Directives>

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