Handling of all day events and auto calendar sync on account creation.

This commit is contained in:
Burak Kaan Köse
2026-03-28 01:44:12 +01:00
parent 686446937b
commit 6f61605c12
16 changed files with 393 additions and 10381 deletions
@@ -33,8 +33,10 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
}
set
{
// When setting from UI (in local time), convert to event's timezone for storage.
CalendarItem.StartDate = value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
// All-day events use floating dates and should not shift across timezones.
CalendarItem.StartDate = CalendarItem.IsAllDayEvent
? value.Date
: value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
}
}
@@ -78,8 +78,12 @@ public static class DateTimeExtensions
}
public static DateTime GetLocalStartDate(this CalendarItem calendarItem)
=> calendarItem.StartDate.ToLocalTimeFromTimeZone(calendarItem.StartTimeZone);
=> calendarItem.IsAllDayEvent
? calendarItem.StartDate
: calendarItem.StartDate.ToLocalTimeFromTimeZone(calendarItem.StartTimeZone);
public static DateTime GetLocalEndDate(this CalendarItem calendarItem)
=> calendarItem.EndDate.ToLocalTimeFromTimeZone(calendarItem.EndTimeZone);
=> calendarItem.IsAllDayEvent
? calendarItem.EndDate
: calendarItem.EndDate.ToLocalTimeFromTimeZone(calendarItem.EndTimeZone);
}
@@ -0,0 +1,61 @@
using FluentAssertions;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Extensions;
using Wino.Core.Domain.Entities.Calendar;
using Google.Apis.Calendar.v3.Data;
using Xunit;
namespace Wino.Core.Tests.Synchronizers;
public sealed class CalendarItemTimeZoneDisplayTests
{
[Fact]
public void AllDayEvents_KeepTheirOriginalCalendarDates_ForDisplay()
{
var calendarItem = new CalendarItem
{
Id = Guid.NewGuid(),
Title = "National Sovereignty and Children's Day",
StartDate = new DateTime(2026, 4, 23, 0, 0, 0),
DurationInSeconds = TimeSpan.FromDays(1).TotalSeconds,
StartTimeZone = "Turkey Standard Time",
EndTimeZone = "Turkey Standard Time"
};
calendarItem.IsAllDayEvent.Should().BeTrue();
calendarItem.LocalStartDate.Should().Be(new DateTime(2026, 4, 23, 0, 0, 0));
calendarItem.LocalEndDate.Should().Be(new DateTime(2026, 4, 24, 0, 0, 0));
}
[Fact]
public void EditingAllDayEventDate_DoesNotApplyTimezoneConversion()
{
var calendarItem = new CalendarItem
{
Id = Guid.NewGuid(),
Title = "Holiday",
StartDate = new DateTime(2026, 4, 23, 0, 0, 0),
DurationInSeconds = TimeSpan.FromDays(1).TotalSeconds,
StartTimeZone = "Turkey Standard Time",
EndTimeZone = "Turkey Standard Time"
};
var viewModel = new CalendarItemViewModel(calendarItem);
viewModel.StartDate = new DateTime(2026, 4, 24, 0, 0, 0);
calendarItem.StartDate.Should().Be(new DateTime(2026, 4, 24, 0, 0, 0));
}
[Fact]
public void GmailDateOnlyEvents_KeepFloatingCalendarDates()
{
var start = new EventDateTime { Date = "2026-04-23" };
var end = new EventDateTime { Date = "2026-04-24" };
GoogleIntegratorExtensions.GetEventLocalDateTime(start).Should().Be(new DateTime(2026, 4, 23, 0, 0, 0));
GoogleIntegratorExtensions.GetEventLocalDateTime(end).Should().Be(new DateTime(2026, 4, 24, 0, 0, 0));
GoogleIntegratorExtensions.GetEventDateTimeOffset(start)!.Value.UtcDateTime.Should().Be(new DateTime(2026, 4, 23, 0, 0, 0, DateTimeKind.Utc));
}
}
@@ -180,6 +180,31 @@ public static class GoogleIntegratorExtensions
return null;
}
public static DateTime? GetEventLocalDateTime(EventDateTime calendarEvent)
{
if (calendarEvent == null)
{
return null;
}
if (calendarEvent.DateTimeDateTimeOffset != null)
{
return DateTime.SpecifyKind(calendarEvent.DateTimeDateTimeOffset.Value.DateTime, DateTimeKind.Unspecified);
}
if (calendarEvent.Date != null)
{
if (DateTime.TryParse(calendarEvent.Date, out DateTime eventDateTime))
{
return DateTime.SpecifyKind(eventDateTime, DateTimeKind.Unspecified);
}
throw new Exception("Invalid date format in Google Calendar event date.");
}
return null;
}
/// <summary>
/// Extracts the timezone string from EventDateTime.
/// Returns null for all-day events or if timezone is not specified.
@@ -335,6 +335,21 @@ public static class OutlookIntegratorExtensions
}
}
public static DateTime GetLocalDateTimeFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
{
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime))
{
throw new ArgumentException("DateTimeTimeZone or DateTime is null or empty.");
}
if (!DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
{
throw new ArgumentException("DateTime string is not in a valid format.");
}
return DateTime.SpecifyKind(parsedDateTime, DateTimeKind.Unspecified);
}
private static AttendeeStatus GetAttendeeStatus(ResponseType? responseType)
{
return responseType switch
@@ -66,12 +66,14 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
// We don't have this event yet. Create a new one.
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
var eventStartLocalDateTime = GoogleIntegratorExtensions.GetEventLocalDateTime(calendarEvent.Start);
var eventEndLocalDateTime = GoogleIntegratorExtensions.GetEventLocalDateTime(calendarEvent.End);
double totalDurationInSeconds = 0;
if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null)
if (eventStartLocalDateTime != null && eventEndLocalDateTime != null)
{
totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds;
totalDurationInSeconds = (eventEndLocalDateTime.Value - eventStartLocalDateTime.Value).TotalSeconds;
}
CalendarItem calendarItem = null;
@@ -97,7 +99,7 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description ?? parentRecurringEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.UtcDateTime,
StartDate = eventStartLocalDateTime.Value,
DurationInSeconds = totalDurationInSeconds,
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
@@ -136,7 +138,7 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.UtcDateTime,
StartDate = eventStartLocalDateTime.Value,
DurationInSeconds = totalDurationInSeconds,
Location = calendarEvent.Location,
@@ -59,14 +59,15 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
savingItem = new CalendarItem() { Id = savingItemId };
}
DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start);
DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End);
var eventStartLocalDateTime = OutlookIntegratorExtensions.GetLocalDateTimeFromDateTimeTimeZone(calendarEvent.Start);
var eventEndLocalDateTime = OutlookIntegratorExtensions.GetLocalDateTimeFromDateTimeTimeZone(calendarEvent.End);
var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
var durationInSeconds = (eventEndLocalDateTime - eventStartLocalDateTime).TotalSeconds;
// Store dates as UTC in the database
// Store the wall-clock values exactly as Outlook returned them for the event timezone.
// Timed events are converted for display later, while all-day events stay as floating dates.
savingItem.RemoteEventId = calendarEvent.Id.WithClientTrackingId(calendarEvent.TransactionId.GetClientTrackingId());
savingItem.StartDate = eventStartDateTimeOffset.UtcDateTime;
savingItem.StartDate = eventStartLocalDateTime;
savingItem.DurationInSeconds = durationInSeconds;
// Store the timezone information from the event
@@ -87,6 +87,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private readonly INativeAppService _nativeAppService;
private readonly IMailService _mailService;
private bool _hasRegisteredPersistentRecipients;
private readonly SemaphoreSlim _menuRefreshSemaphore = new(1, 1);
private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1);
@@ -969,6 +970,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
private async Task RecreateMenuItemsAsync()
{
await _menuRefreshSemaphore.WaitAsync().ConfigureAwait(false);
try
{
await ExecuteUIThread(() =>
{
@@ -978,6 +982,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await LoadAccountsAsync();
}
finally
{
_menuRefreshSemaphore.Release();
}
}
private async Task RestoreSelectedAccountAfterMenuRefreshAsync(bool automaticallyNavigateFirstItem)
{
+24 -4
View File
@@ -239,6 +239,13 @@ public partial class App : WinoApplication,
if (windowManager.GetWindow(WinoWindowKind.Shell) is not ShellWindow shellWindow)
return;
windowManager.HideWindow(shellWindow);
if (ReferenceEquals(MainWindow, shellWindow))
{
MainWindow = null;
InitializeNavigationDispatcher();
}
shellWindow.PrepareForClose();
shellWindow.Close();
}
@@ -748,7 +755,7 @@ public partial class App : WinoApplication,
/// Creates the main window without activating it.
/// Used for both normal launch and startup task launch (tray only).
/// </summary>
private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args)
private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args, string? forcedLaunchArguments = null)
{
LogActivation("Creating main window.");
@@ -761,6 +768,12 @@ public partial class App : WinoApplication,
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Shell, shellWindow.GetMainFrame());
if (!string.IsNullOrWhiteSpace(forcedLaunchArguments))
{
shellWindow.HandleAppActivation(forcedLaunchArguments);
return;
}
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
@@ -902,7 +915,6 @@ public partial class App : WinoApplication,
_hasConfiguredAccounts = true;
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
var navigationService = Services.GetRequiredService<INavigationService>();
// Only transition when the account was created from the WelcomeWindow.
if (windowManager.GetWindow(WinoWindowKind.Welcome) == null)
@@ -911,12 +923,20 @@ public partial class App : WinoApplication,
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
{
// Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher.
CreateWindow(null);
CreateWindow(null, GetModeLaunchArgument(WinoApplicationMode.Mail));
CloseWelcomeWindowIfPresent();
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
if (MainWindow != null)
await ActivateWindowAsync(MainWindow);
if (message.Account.IsCalendarAccessGranted)
{
WeakReferenceMessenger.Default.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
{
AccountId = message.Account.Id,
Type = CalendarSynchronizationType.CalendarEvents
}));
}
RestartAutoSynchronizationLoop();
});
}
@@ -159,8 +159,11 @@
<!-- All-Day template for top area. -->
<VisualState x:Name="AllDayEvent">
<VisualState.Setters>
<Setter Target="EventTitleTextblock.HorizontalAlignment" Value="Stretch" />
<Setter Target="EventTitleTextblock.HorizontalTextAlignment" Value="Left" />
<Setter Target="EventTitleTextblock.Margin" Value="6,0" />
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
<Setter Target="AttributeStack.Margin" Value="0,0,4,0" />
<Setter Target="MainGrid.MinHeight" Value="30" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
</VisualState.Setters>
@@ -52,6 +52,7 @@
<Grid>
<Grid x:Name="TimedRoot" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
@@ -88,8 +89,29 @@
</ItemsControl>
</Grid>
<Grid
<Border
x:Name="TimedAllDayCornerHost"
Grid.Row="1"
Grid.Column="0"
Height="{x:Bind TimedAllDayHeight, Mode=OneWay}"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,1,1"
Visibility="{x:Bind HasTimedAllDayItems, Mode=OneWay}" />
<Grid
x:Name="TimedAllDayHost"
Grid.Row="1"
Grid.Column="1"
Height="{x:Bind TimedAllDayHeight, Mode=OneWay}"
Background="{ThemeResource LayerFillColorDefaultBrush}"
Visibility="{x:Bind HasTimedAllDayItems, Mode=OneWay}">
<skia:SKXamlCanvas x:Name="TimedAllDayCanvas" PaintSurface="TimedAllDayCanvasPaintSurface" />
<Canvas x:Name="TimedAllDayItemsCanvas" />
</Grid>
<Grid
Grid.Row="2"
Grid.ColumnSpan="2"
Background="Transparent">
<ScrollViewer
@@ -51,6 +51,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private MonthCalendarLayoutResult _monthLayout = new(0, 0, [], []);
private INotifyCollectionChanged? _observableItemsSource;
private double _timedDayWidth;
private double _timedAllDayHeight;
private double _monthCellWidth;
private double _monthCellHeight;
private bool _hasPresentedState;
@@ -101,12 +102,14 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private ObservableCollection<HeaderTextLayout> TimedHeaderTextsCollection { get; } = [];
private ObservableCollection<HeaderTextLayout> MonthHeaderTextsCollection { get; } = [];
private ObservableCollection<TimedItemLayout> TimedItemsCollection { get; } = [];
private ObservableCollection<TimedItemLayout> TimedAllDayItemsCollection { get; } = [];
private ObservableCollection<MonthCellLabelLayout> MonthCellLabelsCollection { get; } = [];
private ObservableCollection<MonthItemLayout> MonthItemsCollection { get; } = [];
public IEnumerable<HeaderTextLayout> TimedHeaderTexts => TimedHeaderTextsCollection;
public IEnumerable<HeaderTextLayout> MonthHeaderTexts => MonthHeaderTextsCollection;
public IEnumerable<TimedItemLayout> TimedItems => TimedItemsCollection;
public IEnumerable<TimedItemLayout> TimedAllDayItems => TimedAllDayItemsCollection;
public IEnumerable<MonthCellLabelLayout> MonthCellLabels => MonthCellLabelsCollection;
public IEnumerable<MonthItemLayout> MonthItems => MonthItemsCollection;
@@ -155,6 +158,24 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
}
}
public double TimedAllDayHeight
{
get => _timedAllDayHeight;
private set
{
if (_timedAllDayHeight == value)
{
return;
}
_timedAllDayHeight = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasTimedAllDayItems));
}
}
public bool HasTimedAllDayItems => TimedAllDayHeight > 0d;
public double TimelineHeight => TimedCalendarLayoutCalculator.GetTimelineHeight(GetHourHeight());
partial void OnVisibleRangeChanged(VisibleDateRange? newValue) => RequestRefresh();
@@ -309,9 +330,14 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
var timedSurfaceWidth = GetTimedSurfaceWidth();
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : timedSurfaceWidth / _currentRange.Dates.Count;
TimedAllDayHeight = TimedCalendarLayoutCalculator.GetAllDayHeight(
TimedCalendarLayoutCalculator.GetAllDayLaneCount(_currentRange.Dates, CurrentItems));
TimedScrollContentGrid.Width = ActualWidth;
TimedViewport.Width = timedSurfaceWidth;
TimedViewport.Height = TimelineHeight;
TimedAllDayHost.Width = timedSurfaceWidth;
TimedAllDayItemsCanvas.Width = timedSurfaceWidth;
TimedAllDayItemsCanvas.Height = TimedAllDayHeight;
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, timedSurfaceWidth, GetHourHeight());
@@ -323,6 +349,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
TimedDayWidth)));
var eventTemplate = (DataTemplate)Resources["CalendarEventTemplate"];
ReplaceCollection(TimedAllDayItemsCollection, TimedCalendarLayoutCalculator.CalculateAllDayItems(_currentRange, CurrentItems, timedSurfaceWidth).Select(item =>
{
PrepareDisplayMetadata(item.Item, item.Date);
item.Template = eventTemplate;
return item;
}));
ReplaceCollection(TimedItemsCollection, _timedLayout.Items.Select(item =>
{
PrepareDisplayMetadata(item.Item, item.Date);
@@ -330,8 +362,10 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
return item;
}));
RenderHourLabels();
RenderTimedAllDayItems();
RenderTimedItems();
TimedAllDayCanvas.Invalidate();
TimedHeaderCanvas.Invalidate();
TimedStructureCanvas.Invalidate();
}
@@ -470,6 +504,32 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
canvas.DrawLine(0, height - 1, e.Info.Width, height - 1, borderPaint);
}
private void TimedAllDayCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
{
using var borderPaint = CreateLinePaint();
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
var timedSurfaceWidth = GetTimedSurfaceWidth();
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0 || TimedAllDayHeight <= 0)
{
return;
}
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
var height = e.Info.Height;
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
for (var index = 1; index < _timedLayout.VisibleDates.Count; index++)
{
var x = dayWidth * index;
canvas.DrawLine(x, 0, x, height, borderPaint);
}
canvas.DrawLine(0, height - 1, e.Info.Width, height - 1, borderPaint);
}
private void TimedStructureCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
{
using var linePaint = CreateLinePaint();
@@ -645,6 +705,26 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
}
}
private void RenderTimedAllDayItems()
{
TimedAllDayItemsCanvas.Children.Clear();
foreach (var item in TimedAllDayItemsCollection)
{
var presenter = new ContentPresenter
{
Width = item.Bounds.Width,
Height = item.Bounds.Height,
Content = item.Item,
ContentTemplate = item.Template
};
Canvas.SetLeft(presenter, item.Bounds.X);
Canvas.SetTop(presenter, item.Bounds.Y);
TimedAllDayItemsCanvas.Children.Add(presenter);
}
}
private void RenderMonthCellLabels()
{
MonthCellLabelsCanvas.Children.Clear();
@@ -897,6 +977,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private void ResetTimedVisualState()
{
ResetAnimatedElement(TimedScrollViewer);
ResetAnimatedElement(TimedAllDayHost);
}
private static void StartNavigationTransition(Compositor compositor, Visual visual, int direction, double width)
@@ -926,6 +1007,10 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
var clipInset = (float)Math.Max(18d, Math.Min(64d, width * 0.05d));
StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f, animateScale: false);
if (HasTimedAllDayItems)
{
StartTimedElementTransition(compositor, TimedAllDayHost, 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)
@@ -953,6 +1038,10 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private void StartTimedModeTransition(Compositor compositor)
{
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false);
if (HasTimedAllDayItems)
{
StartTimedElementTransition(compositor, TimedAllDayHost, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false);
}
}
private static void StartRefreshTransition(Compositor compositor, Visual visual)
@@ -968,6 +1057,10 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private void StartTimedRefreshTransition(Compositor compositor)
{
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedScrollViewer), 0.8f, TimeSpan.FromMilliseconds(160));
if (HasTimedAllDayItems)
{
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedAllDayHost), 0.8f, TimeSpan.FromMilliseconds(160));
}
}
private static void PrepareAnimatedVisual(Visual visual, UIElement target)
@@ -29,8 +29,24 @@ internal sealed record TimedCalendarLayoutResult(IReadOnlyList<DateOnly> Visible
internal static class TimedCalendarLayoutCalculator
{
private const double AllDayItemHeight = 28d;
private const double AllDayItemGap = 4d;
private const double AllDaySectionPadding = 6d;
public static double GetTimelineHeight(double hourHeight) => hourHeight * 24d;
public static double GetAllDayHeight(int laneCount)
{
if (laneCount <= 0)
{
return 0d;
}
return (AllDaySectionPadding * 2d) +
(laneCount * AllDayItemHeight) +
((laneCount - 1) * AllDayItemGap);
}
public static TimedCalendarLayoutResult Calculate(VisibleDateRange range, IEnumerable<CalendarItemViewModel> items, double availableWidth, double hourHeight)
{
var visibleDates = range.Dates;
@@ -79,6 +95,11 @@ internal static class TimedCalendarLayoutCalculator
continue;
}
if (item.IsAllDayEvent)
{
continue;
}
var localStart = start.LocalDateTime;
var localEnd = end.LocalDateTime;
@@ -101,6 +122,50 @@ internal static class TimedCalendarLayoutCalculator
return segments;
}
public static IReadOnlyList<TimedItemLayout> CalculateAllDayItems(VisibleDateRange range, IEnumerable<CalendarItemViewModel> items, double availableWidth)
{
var visibleDates = range.Dates;
var dayWidth = visibleDates.Count == 0 ? 0d : availableWidth / visibleDates.Count;
var layouts = new List<TimedItemLayout>();
for (var dayIndex = 0; dayIndex < visibleDates.Count; dayIndex++)
{
var date = visibleDates[dayIndex];
var dayItems = BuildAllDayItems(items, date)
.OrderBy(item => item.StartDate)
.ThenBy(item => item.EndDate)
.ThenBy(item => item.Title)
.ToList();
for (var rowIndex = 0; rowIndex < dayItems.Count; rowIndex++)
{
var y = AllDaySectionPadding + (rowIndex * (AllDayItemHeight + AllDayItemGap));
var x = (dayIndex * dayWidth) + 2d;
var width = Math.Max(0d, dayWidth - 4d);
layouts.Add(new TimedItemLayout(
dayItems[rowIndex],
dayIndex,
date,
new LayoutRect(x, y, width, AllDayItemHeight)));
}
}
return layouts;
}
public static int GetAllDayLaneCount(IReadOnlyList<DateOnly> visibleDates, IEnumerable<CalendarItemViewModel> items)
{
var laneCount = 0;
foreach (var date in visibleDates)
{
laneCount = Math.Max(laneCount, BuildAllDayItems(items, date).Count);
}
return laneCount;
}
private static IEnumerable<List<Segment>> BuildClusters(List<Segment> segments)
{
if (segments.Count == 0)
@@ -130,6 +195,41 @@ internal static class TimedCalendarLayoutCalculator
yield return cluster;
}
private static List<CalendarItemViewModel> BuildAllDayItems(IEnumerable<CalendarItemViewModel> items, DateOnly date)
{
var dayStart = date.ToDateTime(TimeOnly.MinValue);
var dayEnd = dayStart.AddDays(1);
var allDayItems = new List<CalendarItemViewModel>();
foreach (var item in items)
{
if (!item.IsAllDayEvent)
{
continue;
}
if (!CalendarItemAccessor.TryGetTimeRange(item, out var start, out var end))
{
continue;
}
var localStart = start.LocalDateTime;
var localEnd = end.LocalDateTime;
if (localEnd <= localStart)
{
continue;
}
if (localStart < dayEnd && localEnd > dayStart)
{
allDayItems.Add(item);
}
}
return allDayItems;
}
private static void AssignColumns(List<Segment> segments)
{
var columnEnds = new List<double>();
+9 -8
View File
@@ -189,14 +189,7 @@ public class NewThemeService : INewThemeService
public FrameworkElement GetShellRootContent()
{
var window = GetThemeWindow();
if (window is IWinoShellWindow shellWindow)
return shellWindow.GetRootContent();
if (window?.Content is FrameworkElement frameworkElement)
return frameworkElement;
throw new Exception("No root content found");
return TryGetShellRootContent() ?? throw new Exception("No root content found");
}
private bool isInitialized = false;
@@ -680,11 +673,19 @@ public class NewThemeService : INewThemeService
if (window == null)
return null;
try
{
if (window is IWinoShellWindow shellWindow)
return shellWindow.GetRootContent();
return window.Content as FrameworkElement;
}
catch (Exception ex)
{
Debug.WriteLine($"Skipping root content lookup for closed window: {ex.Message}");
return null;
}
}
public async Task ApplyThemeToActiveWindowAsync()
{
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff