From 1adba271e2fcdc634fc1627115cfbfb43589585a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Mon, 23 Mar 2026 14:56:36 +0100 Subject: [PATCH] Calendar rendering implementation. --- AGENTS.md | 5 + .../CalendarSettingsPageViewModel.cs | 104 +- .../Messages/CalendarItemTappedMessage.cs | 6 +- .../Collections/DayRangeCollection.cs | 41 - .../Interfaces/IPreferencesService.cs | 1 + .../Models/Calendar/CalendarDayModel.cs | 26 - .../Models/Calendar/CalendarRenderOptions.cs | 13 - .../Models/Calendar/CalendarSettings.cs | 34 +- .../Models/Calendar/DayHeaderRenderModel.cs | 13 - .../Models/Calendar/DayRangeRenderModel.cs | 53 - .../Translations/en_US/resources.json | 2 + .../CalendarEmptySlotTappedEventArgs.cs | 19 + .../Calendar/CalendarItemControl.xaml.cs | 4 +- .../Calendar/CalendarPeriodControl.xaml | 76 +- .../Calendar/CalendarPeriodControl.xaml.cs | 222 +- .../Controls/Calendar/DayColumnControl.cs | 150 - .../Controls/Calendar/DayHeaderControl.cs | 56 - .../Controls/Calendar/UniformItemsControl.cs | 85 - .../Controls/Calendar/WinoCalendarPanel.cs | 293 - .../WinoCalendarTypeSelectorControl.cs | 52 +- Wino.Mail.WinUI/Controls/ControlConstants.cs | 1 + Wino.Mail.WinUI/Helpers/XamlHelpers.cs | 1 + .../Services/PreferencesService.cs | 9 +- Wino.Mail.WinUI/Styles/DayHeaderControl.xaml | 23 - .../WinoCalendarTypeSelectorControl.xaml | 22 +- .../Views/Calendar/CalendarPage.xaml | 220 +- .../Views/Calendar/CalendarPage.xaml.cs | 73 + .../Views/Calendar/CalendarSettingsPage.xaml | 36 +- Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs | 2 +- Wino.Mail.WinUI/Wino.Mail.WinUI.csproj | 4 - ...ws__Calendar__CalendarPage_xaml.input.json | 5173 +++++++++++++++++ ...ndar__CalendarSettingsPage_xaml.input.json | 5173 +++++++++++++++++ 32 files changed, 11146 insertions(+), 846 deletions(-) delete mode 100644 Wino.Core.Domain/Collections/DayRangeCollection.cs delete mode 100644 Wino.Core.Domain/Models/Calendar/CalendarDayModel.cs delete mode 100644 Wino.Core.Domain/Models/Calendar/CalendarRenderOptions.cs delete mode 100644 Wino.Core.Domain/Models/Calendar/DayHeaderRenderModel.cs delete mode 100644 Wino.Core.Domain/Models/Calendar/DayRangeRenderModel.cs create mode 100644 Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs delete mode 100644 Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs delete mode 100644 Wino.Mail.WinUI/Controls/Calendar/DayHeaderControl.cs delete mode 100644 Wino.Mail.WinUI/Controls/Calendar/UniformItemsControl.cs delete mode 100644 Wino.Mail.WinUI/Controls/Calendar/WinoCalendarPanel.cs delete mode 100644 Wino.Mail.WinUI/Styles/DayHeaderControl.xaml create mode 100644 tmp_Views__Calendar__CalendarPage_xaml.input.json create mode 100644 tmp_Views__Calendar__CalendarSettingsPage_xaml.input.json diff --git a/AGENTS.md b/AGENTS.md index 9c6e2139..a4b3166b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,9 @@ Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replac # Build WinUI project (Debug x64) dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false +# Build WinUI project with diagnostic XAML/compiler logging (use when plain build only shows "XamlCompiler.exe exited with code 1") +dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false "/flp:logfile=winui-build.log;verbosity=diagnostic" /bl:winui-build.binlog + # Run tests (Debug x64) dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64 @@ -36,6 +39,7 @@ dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config - After the first restore, prefer `--no-restore` builds unless package or project references changed - Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context - When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository +- If a WinUI build only reports `XamlCompiler.exe exited with code 1`, rerun with the diagnostic logging command above and inspect the terminal output plus `winui-build.log` for real `WMC`/`WMC1121`/binding diagnostics before guessing ## Architecture @@ -99,6 +103,7 @@ private string searchQuery = string.Empty; - **NEVER** create IValueConverter classes - WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"` - Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}` +- `x:Bind` does not implicitly convert `double` to `GridLength`; when binding `RowDefinition.Height` or `ColumnDefinition.Width`, use a `XamlHelpers` method such as `DoubleToGridLength(...)` ## Localization diff --git a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs index ebc2b30d..1bd20b2e 100644 --- a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs @@ -41,6 +41,12 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel [ObservableProperty] public partial int WorkingDayEndIndex { get; set; } + [ObservableProperty] + public partial string TimedDayHeaderDateFormat { get; set; } = "ddd dd"; + + [ObservableProperty] + public partial int SelectedTimedDayHeaderFormatPresetIndex { get; set; } = -1; + [ObservableProperty] public partial List ReminderOptions { get; set; } = []; @@ -56,6 +62,14 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel public ObservableCollection Accounts { get; } = []; public ObservableCollection NewEventBehaviorOptions { get; } = []; public ObservableCollection AvailableNewEventCalendars { get; } = []; + public ObservableCollection TimedDayHeaderFormatPresets { get; } = + [ + "ddd dd", + "dddd dd", + "ddd d MMM", + "dd MMM ddd", + "M/d ddd" + ]; [ObservableProperty] public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; } @@ -68,6 +82,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel public IPreferencesService PreferencesService { get; } private readonly ICalendarService _calendarService; private readonly IAccountService _accountService; + private readonly CultureInfo _calendarCulture; private readonly bool _isLoaded = false; public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService, IAccountService accountService) @@ -78,6 +93,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage); var cultureInfo = new CultureInfo(currentLanguageLanguageCode); + _calendarCulture = cultureInfo; for (var i = 0; i < 7; i++) { @@ -92,6 +108,8 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel CellHourHeight = preferencesService.HourHeight; WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart)); WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd)); + TimedDayHeaderDateFormat = preferencesService.CalendarTimedDayHeaderDateFormat; + SelectedTimedDayHeaderFormatPresetIndex = TimedDayHeaderFormatPresets.IndexOf(TimedDayHeaderDateFormat); var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes(); ReminderOptions.Add("None"); @@ -169,12 +187,52 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel } partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings(); - partial void OnIs24HourHeadersChanged(bool value) => SaveSettings(); + partial void OnIs24HourHeadersChanged(bool value) + { + OnPropertyChanged(nameof(TimedHourLabelPreview)); + SaveSettings(); + } partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings(); partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings(); partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings(); partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings(); partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings(); + partial void OnTimedDayHeaderDateFormatChanged(string value) + { + OnPropertyChanged(nameof(TimedDayHeaderFormatPreview)); + OnPropertyChanged(nameof(TimedHourLabelPreview)); + + var normalizedFormat = string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim(); + var matchingPresetIndex = TimedDayHeaderFormatPresets + .Select((format, index) => new { format, index }) + .Where(item => string.Equals(item.format, normalizedFormat, StringComparison.Ordinal)) + .Select(item => item.index) + .DefaultIfEmpty(-1) + .First(); + + if (SelectedTimedDayHeaderFormatPresetIndex != matchingPresetIndex) + { + SelectedTimedDayHeaderFormatPresetIndex = matchingPresetIndex; + } + + SaveSettings(); + } + partial void OnSelectedTimedDayHeaderFormatPresetIndexChanged(int value) + { + if (value < 0 || value >= TimedDayHeaderFormatPresets.Count) + { + return; + } + + var selectedPreset = TimedDayHeaderFormatPresets[value]; + + if (string.Equals(TimedDayHeaderDateFormat, selectedPreset, StringComparison.Ordinal)) + { + return; + } + + TimedDayHeaderDateFormat = selectedPreset; + } partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings(); partial void OnSelectedDefaultSnoozeIndexChanged(int value) => SaveSettings(); partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value) @@ -184,6 +242,49 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel } partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value) => SaveSettings(); + public string TimedDayHeaderFormatPreview + { + get + { + var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat.Trim(); + var previewDates = new[] + { + new DateTime(2026, 3, 23), + new DateTime(2026, 3, 24), + new DateTime(2026, 3, 25) + }; + + try + { + return string.Join(" · ", previewDates.Select(date => date.ToString(format, _calendarCulture))); + } + catch (FormatException) + { + return string.Join(" · ", previewDates.Select(date => date.ToString("ddd dd", _calendarCulture))); + } + } + } + + public string TimedHourLabelPreview + { + get + { + var previewHours = new[] { 0, 9, 14, 24 }; + return string.Join(" · ", previewHours.Select(CurrentSettingsPreviewLabel)); + } + } + + private string CurrentSettingsPreviewLabel(int hour) + { + if (Is24HourHeaders) + { + return hour.ToString(_calendarCulture); + } + + var displayHour = hour % 24; + return DateTime.Today.AddHours(displayHour).ToString("h tt", _calendarCulture); + } + public void SaveSettings() { if (!_isLoaded) @@ -229,6 +330,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel PreferencesService.WorkingHourStart = WorkingHourStart; PreferencesService.WorkingHourEnd = WorkingHourEnd; PreferencesService.HourHeight = CellHourHeight; + PreferencesService.CalendarTimedDayHeaderDateFormat = TimedDayHeaderDateFormat; if (SelectedDefaultReminderIndex == 0) { diff --git a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs index 05f2d99c..8419eaba 100644 --- a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs +++ b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs @@ -1,16 +1,12 @@ 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) + public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel) { CalendarItemViewModel = calendarItemViewModel; - ClickedPeriod = clickedPeriod; } public CalendarItemViewModel CalendarItemViewModel { get; } - public CalendarDayModel ClickedPeriod { get; } } diff --git a/Wino.Core.Domain/Collections/DayRangeCollection.cs b/Wino.Core.Domain/Collections/DayRangeCollection.cs deleted file mode 100644 index 89afeb68..00000000 --- a/Wino.Core.Domain/Collections/DayRangeCollection.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Calendar; - -namespace Wino.Core.Domain.Collections; - -public class DayRangeCollection : ObservableRangeCollection -{ - /// - /// Gets the range of dates that are currently displayed in the collection. - /// - public DateRange DisplayRange - { - get - { - if (Count == 0) return null; - - var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate; - var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate; - - return new DateRange(minimumLoadedDate, maximumLoadedDate); - } - } - - public void RemoveCalendarItem(ICalendarItem calendarItem) - { - foreach (var dayRange in this) - { - - } - } - - public void AddCalendarItem(ICalendarItem calendarItem) - { - foreach (var dayRange in this) - { - var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start)); - calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem); - } - } -} diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index 72276eea..6d25429b 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -236,6 +236,7 @@ public interface IPreferencesService : INotifyPropertyChanged DayOfWeek WorkingDayStart { get; set; } DayOfWeek WorkingDayEnd { get; set; } double HourHeight { get; set; } + string CalendarTimedDayHeaderDateFormat { get; set; } /// /// Setting: Default reminder duration in seconds for new calendar events. diff --git a/Wino.Core.Domain/Models/Calendar/CalendarDayModel.cs b/Wino.Core.Domain/Models/Calendar/CalendarDayModel.cs deleted file mode 100644 index 4f3f06a8..00000000 --- a/Wino.Core.Domain/Models/Calendar/CalendarDayModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using Itenso.TimePeriod; -using Wino.Core.Domain.Collections; - -namespace Wino.Core.Domain.Models.Calendar; - -/// -/// Represents a day in the calendar. -/// Can hold events, appointments, wheather status etc. -/// -public class CalendarDayModel -{ - public ITimePeriod Period { get; } - public CalendarEventCollection EventsCollection { get; } - - public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions) - { - RepresentingDate = representingDate; - Period = new TimeRange(representingDate, representingDate.AddDays(1)); - CalendarRenderOptions = calendarRenderOptions; - EventsCollection = new CalendarEventCollection(Period, calendarRenderOptions.CalendarSettings); - } - - public DateTime RepresentingDate { get; } - public CalendarRenderOptions CalendarRenderOptions { get; } -} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarRenderOptions.cs b/Wino.Core.Domain/Models/Calendar/CalendarRenderOptions.cs deleted file mode 100644 index 2f31db08..00000000 --- a/Wino.Core.Domain/Models/Calendar/CalendarRenderOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Wino.Core.Domain.Models.Calendar; - -public class CalendarRenderOptions -{ - public CalendarRenderOptions(DateRange dateRange, CalendarSettings calendarSettings) - { - DateRange = dateRange; - CalendarSettings = calendarSettings; - } - public int TotalDayCount => DateRange.TotalDays; - public DateRange DateRange { get; } - public CalendarSettings CalendarSettings { get; } -} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs b/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs index 034fcf97..ccced811 100644 --- a/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs +++ b/Wino.Core.Domain/Models/Calendar/CalendarSettings.cs @@ -13,7 +13,8 @@ public record CalendarSettings(DayOfWeek FirstDayOfWeek, TimeSpan WorkingHourEnd, double HourHeight, DayHeaderDisplayType DayHeaderDisplayType, - CultureInfo CultureInfo) + CultureInfo CultureInfo, + string TimedDayHeaderDateFormat = "ddd dd") { public int WorkWeekDayCount { @@ -65,4 +66,35 @@ public record CalendarSettings(DayOfWeek FirstDayOfWeek, var dateTime = DateTime.Today.Add(timeSpan); return dateTime.ToString(format, CultureInfo.InvariantCulture); } + + public string GetTimedDayHeaderText(DateOnly date) + { + var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat; + + try + { + return date.ToDateTime(TimeOnly.MinValue).ToString(format, CultureInfo); + } + catch (FormatException) + { + return date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd", CultureInfo); + } + } + + public string GetTimedHourLabelText(int hour) + { + if (hour < 0 || hour > 24) + { + throw new ArgumentOutOfRangeException(nameof(hour)); + } + + if (DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour) + { + return hour.ToString(CultureInfo); + } + + var displayHour = hour % 24; + var dateTime = DateTime.Today.AddHours(displayHour); + return dateTime.ToString("h tt", CultureInfo); + } } diff --git a/Wino.Core.Domain/Models/Calendar/DayHeaderRenderModel.cs b/Wino.Core.Domain/Models/Calendar/DayHeaderRenderModel.cs deleted file mode 100644 index 2df4b2aa..00000000 --- a/Wino.Core.Domain/Models/Calendar/DayHeaderRenderModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Wino.Core.Domain.Models.Calendar; - -public class DayHeaderRenderModel -{ - public DayHeaderRenderModel(string dayHeader, double hourHeight) - { - DayHeader = dayHeader; - HourHeight = hourHeight; - } - - public string DayHeader { get; } - public double HourHeight { get; } -} diff --git a/Wino.Core.Domain/Models/Calendar/DayRangeRenderModel.cs b/Wino.Core.Domain/Models/Calendar/DayRangeRenderModel.cs deleted file mode 100644 index 90a661e2..00000000 --- a/Wino.Core.Domain/Models/Calendar/DayRangeRenderModel.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Itenso.TimePeriod; -using Wino.Core.Domain.Enums; - -namespace Wino.Core.Domain.Models.Calendar; - -/// -/// Represents a range of days in the calendar. -/// Corresponds to 1 view of the FlipView in CalendarPage. -/// -public class DayRangeRenderModel -{ - public ITimePeriod Period { get; } - public List CalendarDays { get; } = []; - - // TODO: Get rid of this at some point. - public List DayHeaders { get; } = []; - public CalendarRenderOptions CalendarRenderOptions { get; } - - public int TotalDays => CalendarRenderOptions.TotalDayCount; - - public DayRangeRenderModel(CalendarRenderOptions calendarRenderOptions) - { - CalendarRenderOptions = calendarRenderOptions; - - for (var i = 0; i < CalendarRenderOptions.TotalDayCount; i++) - { - var representingDate = calendarRenderOptions.DateRange.StartDate.AddDays(i); - var calendarDayModel = new CalendarDayModel(representingDate, calendarRenderOptions); - - CalendarDays.Add(calendarDayModel); - } - - Period = new TimeRange(CalendarDays.First().RepresentingDate, CalendarDays.Last().RepresentingDate.AddDays(1)); - - // Create day headers based on culture info. - - for (var i = 0; i < 24; i++) - { - var representingDate = calendarRenderOptions.DateRange.StartDate.Date.AddHours(i); - - string dayHeader = calendarRenderOptions.CalendarSettings.DayHeaderDisplayType switch - { - DayHeaderDisplayType.TwelveHour => representingDate.ToString("h tt", calendarRenderOptions.CalendarSettings.CultureInfo), - DayHeaderDisplayType.TwentyFourHour => representingDate.ToString("HH", calendarRenderOptions.CalendarSettings.CultureInfo), - _ => "N/A" - }; - - DayHeaders.Add(new DayHeaderRenderModel(dayHeader, calendarRenderOptions.CalendarSettings.HourHeight)); - } - } -} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index a4271cff..57db9af5 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -731,6 +731,8 @@ "SettingsCalendarSettings_Title": "Calendar Settings", "CalendarSettings_DefaultSnoozeDuration_Header": "Default snooze duration", "CalendarSettings_DefaultSnoozeDuration_Description": "Set a default snooze duration for calendar reminder notifications.", + "CalendarSettings_TimedDayHeaderFormat_Header": "Timed view day header format", + "CalendarSettings_TimedDayHeaderFormat_Description": "Choose how the top day labels are rendered in day, week, and work week views. Use date format tokens like ddd, dd, MMM, or dddd.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs new file mode 100644 index 00000000..a1e3fffb --- /dev/null +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.UI.Xaml; +using Windows.Foundation; + +namespace Wino.Calendar.Controls; + +public sealed class CalendarEmptySlotTappedEventArgs : EventArgs +{ + public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point positionerPoint, Size cellSize) + { + ClickedDate = clickedDate; + PositionerPoint = positionerPoint; + CellSize = cellSize; + } + + public DateTime ClickedDate { get; } + public Point PositionerPoint { get; } + public Size CellSize { get; } +} diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs index e9cbe122..09a12209 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs @@ -9,8 +9,6 @@ using Microsoft.UI.Xaml.Media; 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 @@ -96,7 +94,7 @@ public sealed partial class CalendarItemControl : UserControl if (isSingleTap && CalendarItem != null) { - WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, null)); + WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem)); } } diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml index 533b2115..1278966c 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml @@ -4,6 +4,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 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:skia="using:SkiaSharp.Views.Windows" @@ -53,14 +54,34 @@ - + + + + + - + + + @@ -69,17 +90,42 @@ - - - - - - + Grid.ColumnSpan="2" + Background="Transparent"> + + + + + + + + + + + + + + + + + + @@ -106,7 +152,11 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs index 436e7471..c83ce397 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs @@ -12,9 +12,11 @@ using Microsoft.UI.Composition; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using SkiaSharp; using SkiaSharp.Views.Windows; +using Windows.Foundation; using Windows.UI; using Wino.Calendar.ViewModels.Data; using Wino.Core.Domain.Enums; @@ -25,6 +27,9 @@ namespace Wino.Calendar.Controls; public sealed partial class CalendarPeriodControl : UserControl, INotifyPropertyChanged { + private const double TimedHourColumnWidth = 64d; + private const double TimedGridIntervalMinutes = 30d; + private const double TimedSelectionIntervalMinutes = 30d; private VisibleDateRange _currentRange = new( CalendarDisplayType.Month, DateOnly.FromDateTime(DateTime.Today), @@ -62,6 +67,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty public CalendarPeriodControl() => InitializeComponent(); public event PropertyChangedEventHandler? PropertyChanged; + public event EventHandler? EmptySlotTapped; private ObservableCollection TimedHeaderTextsCollection { get; } = []; private ObservableCollection MonthHeaderTextsCollection { get; } = []; @@ -209,20 +215,22 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty { TimedRoot.Visibility = Visibility.Visible; MonthRoot.Visibility = Visibility.Collapsed; + ResetTimedVisualState(); - TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : ActualWidth / _currentRange.Dates.Count; - TimedViewport.Width = ActualWidth; + var timedSurfaceWidth = GetTimedSurfaceWidth(); + + TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : timedSurfaceWidth / _currentRange.Dates.Count; + TimedScrollContentGrid.Width = ActualWidth; + TimedViewport.Width = timedSurfaceWidth; TimedViewport.Height = TimelineHeight; - _timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, ActualWidth, GetHourHeight()); + _timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, timedSurfaceWidth, GetHourHeight()); ReplaceCollection( TimedHeaderTextsCollection, _timedLayout.VisibleDates.Select(date => new HeaderTextLayout( - date.ToDateTime(TimeOnly.MinValue).ToString( - string.IsNullOrWhiteSpace(TimedHeaderDateFormat) ? "ddd dd" : TimedHeaderDateFormat, - CalendarSettings!.CultureInfo), + GetTimedHeaderText(date), TimedDayWidth))); var eventTemplate = (DataTemplate)Resources["CalendarEventTemplate"]; @@ -232,6 +240,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty item.Template = eventTemplate; return item; })); + RenderHourLabels(); RenderTimedItems(); TimedHeaderCanvas.Invalidate(); @@ -352,12 +361,14 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty var canvas = e.Surface.Canvas; canvas.Clear(SKColors.Transparent); - if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0) + var timedSurfaceWidth = GetTimedSurfaceWidth(); + + if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0) { return; } - var scaleX = (float)(e.Info.Width / ActualWidth); + var scaleX = (float)(e.Info.Width / timedSurfaceWidth); var height = e.Info.Height; var dayWidth = (float)(_timedLayout.DayWidth * scaleX); @@ -373,41 +384,50 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private void TimedStructureCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e) { using var linePaint = CreateLinePaint(); + using var minorLinePaint = CreateMinorLinePaint(); using var defaultFillPaint = CreateFillPaint(GetDefaultHourBackground()); using var workFillPaint = CreateFillPaint(GetWorkHourBackground()); var canvas = e.Surface.Canvas; canvas.Clear(SKColors.Transparent); - if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0) + var timedSurfaceWidth = GetTimedSurfaceWidth(); + + if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0) { return; } var hourHeight = GetHourHeight(); var timelineHeight = TimedCalendarLayoutCalculator.GetTimelineHeight(hourHeight); - var scaleX = (float)(e.Info.Width / ActualWidth); + var scaleX = (float)(e.Info.Width / timedSurfaceWidth); var scaleY = (float)(e.Info.Height / timelineHeight); var dayWidth = (float)(_timedLayout.DayWidth * scaleX); var workDayStartHour = CalendarSettings?.WorkingHourStart.TotalHours ?? 9d; var workDayEndHour = CalendarSettings?.WorkingHourEnd.TotalHours ?? 17d; + var intervalHeight = (float)(GetTimedGridIntervalHeight() * scaleY); + var intervalCount = (int)(24d * 60d / TimedGridIntervalMinutes); for (var dayIndex = 0; dayIndex < _timedLayout.VisibleDates.Count; dayIndex++) { var x = dayIndex * dayWidth; + var isWorkingDay = CalendarSettings?.WorkingDays.Contains(_timedLayout.VisibleDates[dayIndex].DayOfWeek) == true; - for (var hour = 0; hour < 24; hour++) + for (var intervalIndex = 0; intervalIndex < intervalCount; intervalIndex++) { - var y = (float)(hour * hourHeight * scaleY); - var scaledHourHeight = (float)(hourHeight * scaleY); - var fillPaint = hour >= workDayStartHour && hour < workDayEndHour ? workFillPaint : defaultFillPaint; - canvas.DrawRect(x, y, dayWidth, scaledHourHeight, fillPaint); + var intervalStartHour = (intervalIndex * TimedGridIntervalMinutes) / 60d; + var y = intervalIndex * intervalHeight; + var fillPaint = isWorkingDay && intervalStartHour >= workDayStartHour && intervalStartHour < workDayEndHour + ? workFillPaint + : defaultFillPaint; + canvas.DrawRect(x, y, dayWidth, intervalHeight, fillPaint); } } - for (var hour = 0; hour <= 24; hour++) + for (var intervalIndex = 0; intervalIndex <= intervalCount; intervalIndex++) { - var y = (float)(hour * hourHeight * scaleY); - canvas.DrawLine(0, y, e.Info.Width, y, linePaint); + var y = intervalIndex * intervalHeight; + var paint = intervalIndex % 2 == 0 ? linePaint : minorLinePaint; + canvas.DrawLine(0, y, e.Info.Width, y, paint); } for (var index = 0; index <= _timedLayout.VisibleDates.Count; index++) @@ -472,6 +492,34 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty } } + private void RenderHourLabels() + { + HourLabelsCanvas.Children.Clear(); + HourLabelsCanvas.Height = TimelineHeight; + + var hourHeight = GetHourHeight(); + var labelWidth = Math.Max(0d, TimedHourColumnWidth - 10d); + + for (var hour = 0; hour <= 24; hour++) + { + var textBlock = new TextBlock + { + Width = labelWidth, + Text = GetTimedHourLabelText(hour), + TextAlignment = TextAlignment.Right, + Opacity = 0.72 + }; + + var y = hour == 24 + ? Math.Max(0d, TimelineHeight - 20d) + : Math.Max(0d, (hour * hourHeight) - 10d); + + Canvas.SetLeft(textBlock, 0d); + Canvas.SetTop(textBlock, y); + HourLabelsCanvas.Children.Add(textBlock); + } + } + private void RenderTimedItems() { TimedItemsCanvas.Children.Clear(); @@ -532,6 +580,70 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty } } + private void TimedInteractionLayerTapped(object sender, TappedRoutedEventArgs e) + { + if (_timedLayout.VisibleDates.Count == 0 || _timedLayout.DayWidth <= 0) + { + return; + } + + var position = e.GetPosition(TimedViewport); + var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1); + var intervalHeight = GetTimedSelectionIntervalHeight(); + var slotIndex = Math.Clamp((int)(position.Y / intervalHeight), 0, (int)((24d * 60d / TimedSelectionIntervalMinutes) - 1)); + var slotStart = TimeSpan.FromMinutes(slotIndex * TimedSelectionIntervalMinutes); + var clickedDate = _timedLayout.VisibleDates[dayIndex].ToDateTime(TimeOnly.MinValue).Add(slotStart); + + EmptySlotTapped?.Invoke( + this, + new CalendarEmptySlotTappedEventArgs( + clickedDate, + new Point(dayIndex * _timedLayout.DayWidth, slotIndex * intervalHeight), + new Size(_timedLayout.DayWidth, intervalHeight))); + } + + private void MonthInteractionLayerTapped(object sender, TappedRoutedEventArgs e) + { + if (_monthLayout.Cells.Count == 0 || _monthLayout.CellWidth <= 0 || _monthLayout.CellHeight <= 0) + { + return; + } + + var position = e.GetPosition(MonthViewport); + var column = Math.Clamp((int)(position.X / _monthLayout.CellWidth), 0, MonthCalendarLayoutCalculator.ColumnCount - 1); + var row = Math.Clamp((int)(position.Y / _monthLayout.CellHeight), 0, MonthCalendarLayoutCalculator.RowCount - 1); + var cellIndex = Math.Clamp((row * MonthCalendarLayoutCalculator.ColumnCount) + column, 0, _monthLayout.Cells.Count - 1); + var cell = _monthLayout.Cells[cellIndex]; + + EmptySlotTapped?.Invoke( + this, + new CalendarEmptySlotTappedEventArgs( + cell.Date.ToDateTime(TimeOnly.MinValue), + new Point(cell.Bounds.X, cell.Bounds.Y), + new Size(cell.Bounds.Width, cell.Bounds.Height))); + } + + private double GetTimedSurfaceWidth() => Math.Max(0d, ActualWidth - TimedHourColumnWidth); + + private string GetTimedHeaderText(DateOnly date) + { + if (!string.IsNullOrWhiteSpace(TimedHeaderDateFormat) && CalendarSettings is not null) + { + try + { + return date.ToDateTime(TimeOnly.MinValue).ToString(TimedHeaderDateFormat, CalendarSettings.CultureInfo); + } + catch (FormatException) + { + } + } + + return CalendarSettings?.GetTimedDayHeaderText(date) ?? date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd"); + } + + private string GetTimedHourLabelText(int hour) + => CalendarSettings?.GetTimedHourLabelText(hour) ?? $"{hour:00}:00"; + private CalendarTransitionInfo GetTransitionInfo() { if (!_hasPresentedState || VisibleRange is null || CalendarSettings is null) @@ -597,11 +709,9 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private void RunTimedTransition(CalendarTransitionInfo transition) { - var headerVisual = ElementCompositionPreview.GetElementVisual(TimedHeaderHost); var contentVisual = ElementCompositionPreview.GetElementVisual(TimedScrollViewer); - var compositor = headerVisual.Compositor; + var compositor = contentVisual.Compositor; - PrepareAnimatedVisual(headerVisual, TimedHeaderHost); PrepareAnimatedVisual(contentVisual, TimedScrollViewer); switch (transition.Kind) @@ -618,6 +728,11 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty } } + private void ResetTimedVisualState() + { + ResetAnimatedElement(TimedScrollViewer); + } + private static void StartNavigationTransition(Compositor compositor, Visual visual, int direction, double width) { var travel = (float)Math.Max(48d, Math.Min(160d, width * 0.08d)); @@ -644,8 +759,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty var signedTravel = direction >= 0 ? travel : -travel; var clipInset = (float)Math.Max(18d, Math.Min(64d, width * 0.05d)); - StartTimedElementTransition(compositor, TimedHeaderHost, signedTravel * 0.45f, 0f, 0.78f, TimeSpan.FromMilliseconds(180), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f); - StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f); + StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f, animateScale: false); } private static void StartModeTransition(Compositor compositor, Visual visual) @@ -672,8 +786,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private void StartTimedModeTransition(Compositor compositor) { - StartTimedElementTransition(compositor, TimedHeaderHost, 0f, 10f, 0f, TimeSpan.FromMilliseconds(180), 0f, 0f); - StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f); + StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false); } private static void StartRefreshTransition(Compositor compositor, Visual visual) @@ -688,7 +801,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private void StartTimedRefreshTransition(Compositor compositor) { - StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedHeaderHost), 0.86f, TimeSpan.FromMilliseconds(140)); StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedScrollViewer), 0.8f, TimeSpan.FromMilliseconds(160)); } @@ -700,7 +812,25 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty visual.StopAnimation(nameof(visual.Scale)); } - private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset) + private static void ResetAnimatedElement(UIElement target) + { + var visual = ElementCompositionPreview.GetElementVisual(target); + PrepareAnimatedVisual(visual, target); + + visual.Offset = Vector3.Zero; + visual.Opacity = 1f; + visual.Scale = new Vector3(1f, 1f, 1f); + + if (visual.Clip is InsetClip clip) + { + clip.StopAnimation(nameof(clip.LeftInset)); + clip.StopAnimation(nameof(clip.RightInset)); + clip.LeftInset = 0f; + clip.RightInset = 0f; + } + } + + private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset, bool animateScale = true) { var visual = ElementCompositionPreview.GetElementVisual(target); PrepareAnimatedVisual(visual, target); @@ -723,11 +853,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty opacityAnimation.InsertKeyFrame(1f, 1f, fadeEasing); opacityAnimation.Duration = duration; - var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); - scaleAnimation.InsertKeyFrame(0f, new Vector3(0.996f, 0.996f, 1f)); - scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f), easing); - scaleAnimation.Duration = duration; - var leftInsetAnimation = compositor.CreateScalarKeyFrameAnimation(); leftInsetAnimation.InsertKeyFrame(1f, 0f, easing); leftInsetAnimation.Duration = duration; @@ -738,7 +863,20 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty visual.StartAnimation(nameof(visual.Offset), offsetAnimation); visual.StartAnimation(nameof(visual.Opacity), opacityAnimation); - visual.StartAnimation(nameof(visual.Scale), scaleAnimation); + + if (animateScale) + { + var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.InsertKeyFrame(0f, new Vector3(0.996f, 0.996f, 1f)); + scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f), easing); + scaleAnimation.Duration = duration; + visual.StartAnimation(nameof(visual.Scale), scaleAnimation); + } + else + { + visual.Scale = new Vector3(1f, 1f, 1f); + } + clip.StartAnimation(nameof(clip.LeftInset), leftInsetAnimation); clip.StartAnimation(nameof(clip.RightInset), rightInsetAnimation); } @@ -768,6 +906,18 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty }; } + private static SKPaint CreateMinorLinePaint() + { + var strokeColor = GetStrokeColor(); + + return new SKPaint + { + Color = new SKColor(strokeColor.R, strokeColor.G, strokeColor.B, (byte)Math.Max(20, strokeColor.A / 4)), + IsAntialias = false, + StrokeWidth = 1 + }; + } + private static SKPaint CreateFillPaint(Brush brush) { return new SKPaint @@ -805,6 +955,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty return new SolidColorBrush(Color.FromArgb(255, 34, 40, 52)); } + private static double GetTimedGridIntervalHeight(double hourHeight) => hourHeight * (TimedGridIntervalMinutes / 60d); + + private double GetTimedGridIntervalHeight() => GetTimedGridIntervalHeight(GetHourHeight()); + + private double GetTimedSelectionIntervalHeight() => GetHourHeight() * (TimedSelectionIntervalMinutes / 60d); + private double GetHourHeight() => CalendarSettings?.HourHeight ?? 60d; private static Color GetStrokeColor() diff --git a/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs b/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs deleted file mode 100644 index 9ac8d81b..00000000 --- a/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Specialized; -using System.Linq; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Wino.Core.Domain.Collections; -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; - private CalendarEventCollection? _boundEventsCollection; - - 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); - Unloaded += OnUnloaded; - } - - 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; - - RegisterEventsCollectionHandlers(); - UpdateValues(); - } - - private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e) - { - if (control is DayColumnControl columnControl) - { - columnControl.RegisterEventsCollectionHandlers(); - columnControl.UpdateValues(); - } - } - - private void OnUnloaded(object sender, RoutedEventArgs e) - { - DeregisterEventsCollectionHandlers(); - } - - private bool IsMonthlyTemplate() => ColumnHeaderText == null; - - private void RegisterEventsCollectionHandlers() - { - var nextCollection = DayModel?.EventsCollection; - if (ReferenceEquals(_boundEventsCollection, nextCollection)) - return; - - DeregisterEventsCollectionHandlers(); - - _boundEventsCollection = nextCollection; - if (_boundEventsCollection == null) - return; - - ((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged += EventsCollectionChanged; - ((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged += EventsCollectionChanged; - } - - private void DeregisterEventsCollectionHandlers() - { - if (_boundEventsCollection == null) - return; - - ((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged -= EventsCollectionChanged; - ((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged -= EventsCollectionChanged; - _boundEventsCollection = null; - } - - private void EventsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - UpdateEventItemsSource(); - } - - private void UpdateEventItemsSource() - { - if (AllDayItemsControl == null || DayModel == null) return; - - if (IsMonthlyTemplate()) - { - // Month cells should show all events for the day, not only all-day/multi-day. - var monthlyItems = DayModel.EventsCollection.AllDayEvents - .Concat(DayModel.EventsCollection.RegularEvents) - .GroupBy(a => a.Id) - .Select(g => g.First()) - .OrderBy(a => a.StartDate) - .ToList(); - - AllDayItemsControl.ItemsSource = monthlyItems; - return; - } - - AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents; - } - - private void UpdateValues() - { - if (DayModel == null) return; - - if (HeaderDateDayText != null) - { - 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); - } - - UpdateEventItemsSource(); - - if (IsTodayBorder == null) return; - bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date; - - VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false); - - UpdateLayout(); - } -} diff --git a/Wino.Mail.WinUI/Controls/Calendar/DayHeaderControl.cs b/Wino.Mail.WinUI/Controls/Calendar/DayHeaderControl.cs deleted file mode 100644 index 675fa753..00000000 --- a/Wino.Mail.WinUI/Controls/Calendar/DayHeaderControl.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.UI.Xaml; -using Microsoft.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"); - } - } -} diff --git a/Wino.Mail.WinUI/Controls/Calendar/UniformItemsControl.cs b/Wino.Mail.WinUI/Controls/Calendar/UniformItemsControl.cs deleted file mode 100644 index 02bd064e..00000000 --- a/Wino.Mail.WinUI/Controls/Calendar/UniformItemsControl.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using CommunityToolkit.WinUI; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Wino.Calendar.Controls; -using Wino.Core.Domain.Models.Calendar; - -namespace Wino.Mail.WinUI.Controls.Calendar; - -/// -/// AOT-Safe ItemsControl for use in UniformGrid panels. -/// -/// -public partial class UniformItemsControl : Grid -{ - [GeneratedDependencyProperty] - public partial DayRangeRenderModel? RenderModel { get; set; } - - [GeneratedDependencyProperty] - public partial List? ItemsSource { get; set; } - - partial void OnRenderModelChanged(DayRangeRenderModel? newValue) - { - if (newValue == null || ItemsSource == null) return; - - AdjustColumns(); - } - - partial void OnItemsSourceChanged(List? newValue) - { - if (newValue == null || ItemsSource == null) return; - - AdjustColumns(); - } - - private void AdjustColumns() - { - if (RenderModel == null || ItemsSource == null) return; - - Children.Clear(); - ColumnDefinitions.Clear(); - - var columns = RenderModel.TotalDays; - - // First divide. - for (int i = 0; i < columns; i++) - { - ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - } - - // Then add items. - for (int i = 0; i < columns; i++) - { - var item = ItemsSource[i]; - - var control = new DayColumnControl() - { - DayModel = item - }; - - SetColumn(control, i); - Children.Add(control); - } - } -} -//public partial class UniformItemsControl : ItemsControl -//{ -// private const string ControlUniformGridName = "PART_UniformGrid"; - -// [GeneratedDependencyProperty] -// public partial DayRangeRenderModel? RenderModel { get; set; } - -// partial void OnRenderModelChanged(DayRangeRenderModel? newValue) -// { -// if (newValue == null) return; - -// // Adjust the ItemsPanel based on the RenderModel's columns. -// var uniGrid = WinoVisualTreeHelper.FindDescendants(this); - -// //if (uniGrid != null) -// //{ -// // uniGrid.Columns = newValue.TotalDays; -// //} -// } -//} diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarPanel.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarPanel.cs deleted file mode 100644 index e62e50eb..00000000 --- a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarPanel.cs +++ /dev/null @@ -1,293 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Linq; -using CommunityToolkit.WinUI; -using Itenso.TimePeriod; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Windows.Foundation; -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 _measurements = new Dictionary(); - - 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(); - - if (!calendarControls.Any()) return base.ArrangeOverride(finalSize); - - var events = calendarControls.Select(a => a.Content as CalendarItemViewModel).OfType(); - - 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 events) - { - var columns = new List>(); - 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 { 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> 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> 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 -} diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs index 7403eeb6..3e2730aa 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs @@ -1,8 +1,9 @@ -using System.Windows.Input; +using System.Windows.Input; using CommunityToolkit.Diagnostics; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Enums; +using Wino.Mail.WinUI.Controls; namespace Wino.Calendar.Controls; @@ -11,6 +12,7 @@ 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_WorkWeekToggle = nameof(PART_WorkWeekToggle); private const string PART_MonthToggle = nameof(PART_MonthToggle); public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register( @@ -42,6 +44,7 @@ public partial class WinoCalendarTypeSelectorControl : Control private AppBarButton? _todayButton; private AppBarToggleButton? _dayToggle; private AppBarToggleButton? _weekToggle; + private AppBarToggleButton? _workWeekToggle; private AppBarToggleButton? _monthToggle; public WinoCalendarTypeSelectorControl() @@ -58,24 +61,34 @@ public partial class WinoCalendarTypeSelectorControl : Control _todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton; _dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton; _weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton; + _workWeekToggle = GetTemplateChild(PART_WorkWeekToggle) as AppBarToggleButton; _monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton; Guard.IsNotNull(_todayButton, nameof(_todayButton)); Guard.IsNotNull(_dayToggle, nameof(_dayToggle)); Guard.IsNotNull(_weekToggle, nameof(_weekToggle)); + Guard.IsNotNull(_workWeekToggle, nameof(_workWeekToggle)); Guard.IsNotNull(_monthToggle, nameof(_monthToggle)); _todayButton!.Click += TodayClicked; - - _dayToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); }; - _weekToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); }; - _monthToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); }; + _dayToggle!.Click += DayToggleClicked; + _weekToggle!.Click += WeekToggleClicked; + _workWeekToggle!.Click += WorkWeekToggleClicked; + _monthToggle!.Click += MonthToggleClicked; UpdateToggleButtonStates(); } private void TodayClicked(object? sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null); + private void DayToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Day); + + private void WeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Week); + + private void WorkWeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.WorkWeek); + + private void MonthToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Month); + private void SetSelectedType(CalendarDisplayType type) { SelectedType = type; @@ -84,8 +97,10 @@ public partial class WinoCalendarTypeSelectorControl : Control private static void OnSelectedTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var control = d as WinoCalendarTypeSelectorControl; - control?.UpdateToggleButtonStates(); + if (d is WinoCalendarTypeSelectorControl control) + { + control.UpdateToggleButtonStates(); + } } private void UnregisterHandlers() @@ -94,17 +109,38 @@ public partial class WinoCalendarTypeSelectorControl : Control { _todayButton.Click -= TodayClicked; } + + if (_dayToggle != null) + { + _dayToggle.Click -= DayToggleClicked; + } + + if (_weekToggle != null) + { + _weekToggle.Click -= WeekToggleClicked; + } + + if (_workWeekToggle != null) + { + _workWeekToggle.Click -= WorkWeekToggleClicked; + } + + if (_monthToggle != null) + { + _monthToggle.Click -= MonthToggleClicked; + } } private void UpdateToggleButtonStates() { - if (_dayToggle == null || _weekToggle == null || _monthToggle == null) + if (_dayToggle == null || _weekToggle == null || _workWeekToggle == null || _monthToggle == null) { return; } _dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day; _weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week; + _workWeekToggle.IsChecked = SelectedType == CalendarDisplayType.WorkWeek; _monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month; } } diff --git a/Wino.Mail.WinUI/Controls/ControlConstants.cs b/Wino.Mail.WinUI/Controls/ControlConstants.cs index 0e7d8887..b66f7e43 100644 --- a/Wino.Mail.WinUI/Controls/ControlConstants.cs +++ b/Wino.Mail.WinUI/Controls/ControlConstants.cs @@ -76,6 +76,7 @@ public static class ControlConstants { WinoIconGlyph.CalendarToday, "\uE911" }, { WinoIconGlyph.CalendarDay, "\uE913" }, { WinoIconGlyph.CalendarWeek, "\uE914" }, + { WinoIconGlyph.CalendarWorkWeek, "\uE914" }, { WinoIconGlyph.CalendarMonth, "\uE91c" }, { WinoIconGlyph.CalendarYear, "\uE917" }, { WinoIconGlyph.WeatherBlow, "\uE907" }, diff --git a/Wino.Mail.WinUI/Helpers/XamlHelpers.cs b/Wino.Mail.WinUI/Helpers/XamlHelpers.cs index eb63ddca..60f0f076 100644 --- a/Wino.Mail.WinUI/Helpers/XamlHelpers.cs +++ b/Wino.Mail.WinUI/Helpers/XamlHelpers.cs @@ -43,6 +43,7 @@ public static class XamlHelpers public static Visibility ReverseBoolToVisibilityConverter(bool value) => value ? Visibility.Collapsed : Visibility.Visible; public static Visibility ReverseVisibilityConverter(Visibility visibility) => visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; public static bool ReverseBoolConverter(bool value) => !value; + public static GridLength DoubleToGridLength(double value) => new(value); public static bool AreEqual(int value1, int value2) => value1 == value2; public static bool ShouldDisplayPreview(string text) => text == null ? false : text.Any(x => char.IsLetter(x)); public static bool CountToBooleanConverter(int value) => value > 0; diff --git a/Wino.Mail.WinUI/Services/PreferencesService.cs b/Wino.Mail.WinUI/Services/PreferencesService.cs index 34df1f1f..a5a1dd63 100644 --- a/Wino.Mail.WinUI/Services/PreferencesService.cs +++ b/Wino.Mail.WinUI/Services/PreferencesService.cs @@ -311,6 +311,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob set => SaveProperty(propertyName: nameof(HourHeight), value); } + public string CalendarTimedDayHeaderDateFormat + { + get => _configurationService.Get(nameof(CalendarTimedDayHeaderDateFormat), "ddd dd"); + set => SaveProperty(propertyName: nameof(CalendarTimedDayHeaderDateFormat), string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim()); + } + public TimeSpan WorkingHourStart { get => _configurationService.Get(nameof(WorkingHourStart), new TimeSpan(8, 0, 0)); @@ -402,7 +408,8 @@ public class PreferencesService(IConfigurationService configurationService) : Ob WorkingHourEnd, HourHeight, Prefer24HourTimeFormat ? DayHeaderDisplayType.TwentyFourHour : DayHeaderDisplayType.TwelveHour, - new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage))); + new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)), + CalendarTimedDayHeaderDateFormat); } private List GetDaysBetween(DayOfWeek startDay, DayOfWeek endDay) diff --git a/Wino.Mail.WinUI/Styles/DayHeaderControl.xaml b/Wino.Mail.WinUI/Styles/DayHeaderControl.xaml deleted file mode 100644 index 396a0cf0..00000000 --- a/Wino.Mail.WinUI/Styles/DayHeaderControl.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml b/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml index f5af97aa..02e8caf8 100644 --- a/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml +++ b/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml @@ -2,8 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Wino.Calendar.Controls" - xmlns:controls1="using:Wino.Mail.WinUI.Controls" - xmlns:muxc="using:Microsoft.UI.Xaml.Controls"> + xmlns:controls1="using:Wino.Mail.WinUI.Controls"> - diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml index 76bb0119..f14ad3b4 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml @@ -4,16 +4,224 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:abstract="using:Wino.Calendar.Views.Abstract" xmlns:calendarControls="using:Wino.Calendar.Controls" + xmlns:collections="using:CommunityToolkit.Mvvm.Collections" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:data="using:Wino.Calendar.ViewModels.Data" + xmlns:domain="using:Wino.Core.Domain" + xmlns:helpers="using:Wino.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +