Reminders.

This commit is contained in:
Burak Kaan Köse
2026-01-01 15:02:40 +01:00
parent 3b485dc1fe
commit a64627e7d6
14 changed files with 420 additions and 24 deletions
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
@@ -35,13 +36,22 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
[ObservableProperty] [ObservableProperty]
public partial int WorkingDayEndIndex { get; set; } public partial int WorkingDayEndIndex { get; set; }
[ObservableProperty]
public partial List<string> ReminderOptions { get; set; } = [];
[ObservableProperty]
public partial int SelectedDefaultReminderIndex { get; set; }
public IPreferencesService PreferencesService { get; } public IPreferencesService PreferencesService { get; }
private readonly ICalendarService _calendarService;
private readonly bool _isLoaded = false; private readonly bool _isLoaded = false;
public CalendarSettingsPageViewModel(IPreferencesService preferencesService) public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService)
{ {
PreferencesService = preferencesService; PreferencesService = preferencesService;
_calendarService = calendarService;
var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage); var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
@@ -62,6 +72,31 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart)); WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd)); WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
// Initialize reminder options
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
ReminderOptions.Add("None");
foreach (var minutes in predefinedMinutes)
{
var displayText = minutes switch
{
>= 60 => $"{minutes / 60} Hour{(minutes / 60 > 1 ? "s" : "")}",
_ => $"{minutes} Minute{(minutes > 1 ? "s" : "")}"
};
ReminderOptions.Add(displayText);
}
// Set selected index based on current default reminder setting
if (preferencesService.DefaultReminderDurationInSeconds == 0)
{
SelectedDefaultReminderIndex = 0; // None
}
else
{
var minutes = (int)(preferencesService.DefaultReminderDurationInSeconds / 60);
var index = Array.IndexOf(predefinedMinutes, minutes);
SelectedDefaultReminderIndex = index >= 0 ? index + 1 : 0;
}
_isLoaded = true; _isLoaded = true;
} }
@@ -72,6 +107,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings(); partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings(); partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings(); partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings();
public void SaveSettings() public void SaveSettings()
{ {
@@ -118,6 +154,18 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
PreferencesService.WorkingHourEnd = WorkingHourEnd; PreferencesService.WorkingHourEnd = WorkingHourEnd;
PreferencesService.HourHeight = CellHourHeight; PreferencesService.HourHeight = CellHourHeight;
// Save default reminder setting
if (SelectedDefaultReminderIndex == 0)
{
PreferencesService.DefaultReminderDurationInSeconds = 0; // None
}
else
{
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
var minutes = predefinedMinutes[SelectedDefaultReminderIndex - 1];
PreferencesService.DefaultReminderDurationInSeconds = minutes * 60;
}
Messenger.Send(new CalendarSettingsUpdatedMessage()); Messenger.Send(new CalendarSettingsUpdatedMessage());
} }
} }
@@ -1,12 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
@@ -32,10 +35,14 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))] [NotifyPropertyChangedFor(nameof(CanViewSeries))]
[NotifyPropertyChangedFor(nameof(CanEditSeries))] [NotifyPropertyChangedFor(nameof(CanEditSeries))]
private CalendarItemViewModel _currentEvent; [NotifyPropertyChangedFor(nameof(IsCurrentUserOrganizer))]
public partial CalendarItemViewModel CurrentEvent { get; set; }
[ObservableProperty] [ObservableProperty]
private CalendarItemViewModel _seriesParent; public partial CalendarItemViewModel SeriesParent { get; set; }
[ObservableProperty]
public partial List<Reminder> Reminders { get; set; }
public ObservableCollection<ReminderOption> ReminderOptions { get; } = new ObservableCollection<ReminderOption>();
/// <summary> /// <summary>
/// Returns true if the event is part of a recurring series (as a child occurrence). /// Returns true if the event is part of a recurring series (as a child occurrence).
@@ -49,6 +56,12 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
/// </summary> /// </summary>
public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false; public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false;
/// <summary>
/// Returns true if the current user is the organizer of the event.
/// Used to determine if the user can invite attendees or modify the event.
/// </summary>
public bool IsCurrentUserOrganizer => CurrentEvent?.Attendees?.Any(a => a.IsOrganizer) ?? true;
#endregion #endregion
#region Show As Options #region Show As Options
@@ -113,6 +126,10 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
{ {
CurrentEvent.Attendees.Add(item); CurrentEvent.Attendees.Add(item);
} }
// Load reminders for this calendar item
Reminders = await _calendarService.GetRemindersAsync(currentEventItem.EventTrackingId);
InitializeReminderOptions();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -120,17 +137,88 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
} }
} }
public override void OnNavigatedFrom(NavigationMode mode, object parameters) private void InitializeReminderOptions()
{ {
base.OnNavigatedFrom(mode, parameters); ReminderOptions.Clear();
Messenger.Send(new DetailsPageStateChangedMessage(false)); // Add predefined options from service
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
var predefinedOptions = predefinedMinutes.Select(m => new ReminderOption(m)).ToList();
// Add custom reminders from synced data
if (Reminders != null)
{
foreach (var reminder in Reminders)
{
// Convert seconds to minutes
var minutesDiff = (int)(reminder.DurationInSeconds / 60);
// Check if this is a custom value not in predefined list
if (!predefinedMinutes.Contains(minutesDiff))
{
predefinedOptions.Add(new ReminderOption(minutesDiff, isCustom: true));
}
}
}
// Sort by minutes descending and add to collection
foreach (var option in predefinedOptions.OrderByDescending(o => o.Minutes))
{
ReminderOptions.Add(option);
}
// Set selected state based on current reminders
if (Reminders != null)
{
foreach (var reminder in Reminders)
{
// Convert seconds to minutes
var minutesDiff = (int)(reminder.DurationInSeconds / 60);
var matchingOption = ReminderOptions.FirstOrDefault(o => o.Minutes == minutesDiff);
matchingOption?.IsSelected = true;
}
}
} }
[RelayCommand] [RelayCommand]
private async Task SaveAsync() private async Task SaveAsync()
{ {
// TODO: Implement saving if (CurrentEvent == null) return;
try
{
// Get selected reminder options
var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList();
// Create separate Reminder entities for each selected option
var newReminders = new List<Reminder>();
foreach (var option in selectedOptions)
{
var durationInSeconds = option.Minutes * 60; // Convert minutes to seconds
newReminders.Add(new Reminder
{
Id = Guid.NewGuid(),
CalendarItemId = CurrentEvent.Id,
DurationInSeconds = durationInSeconds,
ReminderType = CalendarItemReminderType.Popup
});
}
// Save reminders to database
await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.EventTrackingId, newReminders);
Reminders = newReminders;
_navigationService.GoBack();
// TODO: Implement saving other event details
}
catch (Exception ex)
{
Debug.WriteLine($"Error saving event: {ex.Message}");
}
} }
[RelayCommand] [RelayCommand]
@@ -183,4 +271,55 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
// TODO: Implement response // TODO: Implement response
} }
[RelayCommand]
private async Task ViewSeriesAsync()
{
if (CurrentEvent == null || !CurrentEvent.IsRecurringChild) return;
try
{
// Get the master event from the recurring series
var masterEventId = CurrentEvent.CalendarItem.RecurringCalendarItemId.Value;
var masterEvent = await _calendarService.GetCalendarItemAsync(masterEventId);
if (masterEvent == null) return;
// Load the master event without navigation
var target = new CalendarItemTarget(masterEvent, CalendarEventTargetType.Series);
await LoadCalendarItemTargetAsync(target);
}
catch (Exception ex)
{
Debug.WriteLine($"Error loading series: {ex.Message}");
}
}
}
public partial class ReminderOption : ObservableObject
{
public int Minutes { get; }
public bool IsCustom { get; }
[ObservableProperty]
public partial bool IsSelected { get; set; }
public string DisplayText
{
get
{
if (Minutes >= 60)
{
var hours = Minutes / 60;
return hours == 1 ? "1 Hour" : $"{hours} Hours";
}
return Minutes == 1 ? "1 Minute" : $"{Minutes} Minutes";
}
}
public ReminderOption(int minutes, bool isCustom = false)
{
Minutes = minutes;
IsCustom = isCustom;
}
} }
@@ -10,6 +10,10 @@ public class Reminder
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid CalendarItemId { get; set; } public Guid CalendarItemId { get; set; }
public DateTimeOffset ReminderTime { get; set; } /// <summary>
/// Duration in seconds before the event start time when the reminder should trigger.
/// For example, 900 seconds = 15 minutes before event.
/// </summary>
public long DurationInSeconds { get; set; }
public CalendarItemReminderType ReminderType { get; set; } public CalendarItemReminderType ReminderType { get; set; }
} }
@@ -47,4 +47,11 @@ public interface ICalendarService
Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId); Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId);
Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees); Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees);
Task UpdateCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees); Task UpdateCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
Task<List<Reminder>> GetRemindersAsync(Guid calendarItemId);
Task SaveRemindersAsync(Guid calendarItemId, List<Reminder> reminders);
/// <summary>
/// Gets predefined reminder options in minutes (1 Hour, 30 Min, 15 Min, 5 Min, 1 Min).
/// </summary>
int[] GetPredefinedReminderMinutes();
} }
@@ -216,6 +216,11 @@ public interface IPreferencesService : INotifyPropertyChanged
DayOfWeek WorkingDayEnd { get; set; } DayOfWeek WorkingDayEnd { get; set; }
double HourHeight { get; set; } double HourHeight { get; set; }
/// <summary>
/// Setting: Default reminder duration in seconds for new calendar events.
/// Set to 0 to disable default reminders.
/// </summary>
long DefaultReminderDurationInSeconds { get; set; }
CalendarSettings GetCurrentCalendarSettings(); CalendarSettings GetCurrentCalendarSettings();
@@ -223,7 +223,37 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
} }
} }
// Prepare reminders list from Gmail event
List<Reminder> reminders = null;
if (calendarEvent.Reminders?.Overrides != null && calendarEvent.Reminders.Overrides.Count > 0)
{
reminders = new List<Reminder>();
foreach (var reminderOverride in calendarEvent.Reminders.Overrides)
{
if (reminderOverride.Minutes.HasValue)
{
var durationInSeconds = reminderOverride.Minutes.Value * 60; // Convert minutes to seconds
var reminderType = reminderOverride.Method switch
{
"email" => CalendarItemReminderType.Email,
_ => CalendarItemReminderType.Popup
};
reminders.Add(new Reminder
{
Id = Guid.NewGuid(),
CalendarItemId = calendarItem.Id,
DurationInSeconds = durationInSeconds,
ReminderType = reminderType
});
}
}
}
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees); await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
// Save reminders separately
await CalendarService.SaveRemindersAsync(calendarItem.Id, reminders).ConfigureAwait(false);
} }
else else
{ {
@@ -255,6 +285,36 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
// Update the event properties. // Update the event properties.
} }
// Prepare reminders list from Gmail event for update
List<Reminder> reminders = null;
if (calendarEvent.Reminders?.Overrides != null && calendarEvent.Reminders.Overrides.Count > 0)
{
reminders = new List<Reminder>();
foreach (var reminderOverride in calendarEvent.Reminders.Overrides)
{
if (reminderOverride.Minutes.HasValue)
{
var durationInSeconds = reminderOverride.Minutes.Value * 60; // Convert minutes to seconds
var reminderType = reminderOverride.Method switch
{
"email" => CalendarItemReminderType.Email,
_ => CalendarItemReminderType.Popup
};
reminders.Add(new Reminder
{
Id = Guid.NewGuid(),
CalendarItemId = existingCalendarItem.Id,
DurationInSeconds = durationInSeconds,
ReminderType = reminderType
});
}
}
}
// Save reminders
await CalendarService.SaveRemindersAsync(existingCalendarItem.Id, reminders).ConfigureAwait(false);
} }
// Upsert the event. // Upsert the event.
@@ -10,6 +10,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Services; using Wino.Services;
using Reminder = Wino.Core.Domain.Entities.Calendar.Reminder;
namespace Wino.Core.Integration.Processors; namespace Wino.Core.Integration.Processors;
@@ -175,6 +176,25 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId, organizerEmail)).ToList(); attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId, organizerEmail)).ToList();
} }
// Prepare reminders list from Outlook event
List<Reminder> reminders = null;
if (calendarEvent.IsReminderOn.GetValueOrDefault() && calendarEvent.ReminderMinutesBeforeStart.HasValue)
{
var reminderMinutes = calendarEvent.ReminderMinutesBeforeStart.Value;
var reminderDurationInSeconds = reminderMinutes * 60; // Convert minutes to seconds
reminders = new List<Reminder>
{
new Reminder
{
Id = Guid.NewGuid(),
CalendarItemId = savingItemId,
DurationInSeconds = reminderDurationInSeconds,
ReminderType = CalendarItemReminderType.Popup
}
};
}
// Use CalendarService to create or update the event // Use CalendarService to create or update the event
if (isNewItem) if (isNewItem)
{ {
@@ -186,5 +206,8 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
// Existing item - use UpdateCalendarItemAsync // Existing item - use UpdateCalendarItemAsync
await CalendarService.UpdateCalendarItemAsync(savingItem, attendees).ConfigureAwait(false); await CalendarService.UpdateCalendarItemAsync(savingItem, attendees).ConfigureAwait(false);
} }
// Save reminders separately
await CalendarService.SaveRemindersAsync(savingItemId, reminders).ConfigureAwait(false);
} }
} }
@@ -42,6 +42,7 @@ public static class CalendarXamlHelpers
public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel) public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel)
{ {
// TODO: This is incorrect.
if (calendarItemViewModel == null || string.IsNullOrEmpty(calendarItemViewModel.CalendarItem.Recurrence)) return string.Empty; if (calendarItemViewModel == null || string.IsNullOrEmpty(calendarItemViewModel.CalendarItem.Recurrence)) return string.Empty;
// Parse recurrence rules // Parse recurrence rules
@@ -27,7 +27,7 @@ public class PreDefinedAppTheme : AppThemeBase
public override async Task<string> GetThemeResourceDictionaryContentAsync() public override async Task<string> GetThemeResourceDictionaryContentAsync()
{ {
var xamlDictionaryFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///Wino.Mail.WinUI/AppThemes/{ThemeName}.xaml")); var xamlDictionaryFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx://AppThemes/{ThemeName}.xaml"));
return await FileIO.ReadTextAsync(xamlDictionaryFile); return await FileIO.ReadTextAsync(xamlDictionaryFile);
} }
} }
@@ -284,6 +284,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SaveProperty(propertyName: nameof(WorkingDayEnd), value); set => SaveProperty(propertyName: nameof(WorkingDayEnd), value);
} }
public long DefaultReminderDurationInSeconds
{
get => _configurationService.Get(nameof(DefaultReminderDurationInSeconds), 900L); // Default: 15 minutes (900 seconds)
set => SaveProperty(propertyName: nameof(DefaultReminderDurationInSeconds), value);
}
public int EmailSyncIntervalMinutes public int EmailSyncIntervalMinutes
{ {
get => _configurationService.Get(nameof(EmailSyncIntervalMinutes), 3); get => _configurationService.Get(nameof(EmailSyncIntervalMinutes), 3);
@@ -178,6 +178,16 @@
</controls:SettingsCard> </controls:SettingsCard>
</controls:SettingsExpander.Items> </controls:SettingsExpander.Items>
</controls:SettingsExpander> </controls:SettingsExpander>
<!-- Default reminder -->
<controls:SettingsCard Description="Set a default reminder for all new calendar events." Header="Default reminder">
<controls:SettingsCard.HeaderIcon>
<PathIcon Data="F1 M 10 1.25 C 10.456706 1.25 10.889486 1.337565 11.298339 1.512695 C 11.707192 1.687826 12.072591 1.927409 12.394531 2.231445 C 12.716471 2.535482 12.97656 2.892253 13.173828 3.301758 C 13.371096 3.711263 13.481771 4.146484 13.505859 4.607422 L 13.505859 5 C 13.969401 5.028646 14.386068 5.16276 14.755859 5.402344 C 15.125651 5.641927 15.414713 5.947917 15.623047 6.320312 C 15.83138 6.692709 15.9349 7.096355 15.933594 7.53125 L 15.933594 8.75 L 16.25 8.75 C 16.822917 8.75 17.317057 8.95638 17.732422 9.369141 C 18.147787 9.781901 18.357422 10.273437 18.359375 10.84375 L 18.359375 16.40625 C 18.359375 16.979167 18.153971 17.473308 17.743164 17.888672 C 17.332357 18.304037 16.839844 18.511719 16.265625 18.511719 L 3.734375 18.511719 C 3.164062 18.511719 2.671549 18.304037 2.256836 17.888672 C 1.842122 17.473308 1.634114 16.979167 1.630859 16.40625 L 1.630859 10.84375 C 1.630859 10.273437 1.837565 9.781901 2.250977 9.369141 C 2.664388 8.95638 3.156901 8.75 3.728516 8.75 L 4.0625 8.75 L 4.0625 7.53125 C 4.0625 7.09375 4.166341 6.689453 4.374023 6.314453 C 4.581706 5.939453 4.86914 5.632486 5.236328 5.393555 C 5.603516 5.154623 6.019532 5.021485 6.484375 4.994141 L 6.484375 4.607422 C 6.506511 4.148438 6.617838 3.714518 6.818359 3.305664 C 7.01888 2.896811 7.282877 2.539063 7.610352 2.232422 C 7.937826 1.92578 8.308268 1.685222 8.721679 1.510742 C 9.135091 1.336264 9.56575 1.249024 10.013672 1.249024 Z M 10.013672 2.5 C 9.441406 2.5 8.947916 2.706381 8.533203 3.119141 C 8.118489 3.531901 7.911459 4.023437 7.912109 4.59375 L 7.912109 6.25 L 12.089844 6.25 L 12.089844 4.59375 C 12.089844 4.023438 11.882161 3.531902 11.466797 3.119141 C 11.051433 2.706382 10.557292 2.5 9.984375 2.5 Z M 5.3125 7.53125 L 5.3125 8.75 L 14.6875 8.75 L 14.6875 7.53125 C 14.6875 7.303385 14.605144 7.107747 14.440429 6.944336 C 14.275714 6.780925 14.080729 6.69987 13.855469 6.703125 L 6.142578 6.703125 C 5.914713 6.703125 5.71875 6.785482 5.552734 6.950195 C 5.386719 7.114909 5.30339 7.310547 5.300781 7.537109 Z M 16.25 10 L 3.75 10 C 3.540365 10 3.361328 10.071615 3.212891 10.214844 C 3.064453 10.358073 2.989258 10.534505 2.986328 10.744141 L 2.986328 16.40625 C 2.986328 16.618489 3.058595 16.797526 3.203125 16.943359 C 3.347656 17.089193 3.526693 17.161458 3.740234 17.15625 L 16.25 17.15625 C 16.458333 17.15625 16.636067 17.082683 16.783203 16.935547 C 16.930339 16.788411 17.003906 16.609375 17.003906 16.398437 L 17.003906 10.742187 C 17.003906 10.536459 16.932942 10.359049 16.791016 10.209961 C 16.64909 10.060873 16.469401 9.986328 16.257812 9.986328 Z M 10 11.5625 C 10.227865 11.5625 10.423502 11.644856 10.586914 11.809571 C 10.750325 11.974285 10.83138 12.16862 10.830078 12.393555 L 10.830078 13.427734 L 11.855469 13.427734 C 12.080404 13.427734 12.273437 13.509115 12.434571 13.671875 C 12.595704 13.834636 12.675781 14.026693 12.675781 14.248047 C 12.675781 14.46224 12.596354 14.653321 12.4375 14.813477 C 12.278646 14.973633 12.089192 15.053711 11.869141 15.053711 L 10.830078 15.053711 L 10.830078 16.074219 C 10.830078 16.296224 10.748372 16.486981 10.584961 16.646484 C 10.42155 16.805989 10.229818 16.885742 10.005859 16.885742 C 9.781901 16.885742 9.590495 16.806315 9.431641 16.647461 C 9.272786 16.488607 9.193359 16.296875 9.193359 16.072266 L 9.193359 15.053711 L 8.15625 15.053711 C 7.931314 15.053711 7.738934 14.972331 7.579102 14.80957 C 7.419269 14.646809 7.339192 14.454753 7.338867 14.233399 C 7.338867 14.008464 7.419921 13.815755 7.582031 13.655274 C 7.744141 13.494793 7.935872 13.414551 8.157227 13.414551 L 9.193359 13.414551 L 9.193359 12.394531 C 9.193359 12.166667 9.275715 11.971029 9.440429 11.807618 C 9.605144 11.644206 9.799479 11.562825 10.023437 11.5625 Z" />
</controls:SettingsCard.HeaderIcon>
<controls:SettingsCard.Content>
<ComboBox ItemsSource="{x:Bind ViewModel.ReminderOptions, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SelectedDefaultReminderIndex, Mode=TwoWay}" />
</controls:SettingsCard.Content>
</controls:SettingsCard>
</StackPanel> </StackPanel>
<VisualStateManager.VisualStateGroups> <VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ClockIdentifierStates"> <VisualStateGroup x:Name="ClockIdentifierStates">
@@ -6,6 +6,7 @@
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract" xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar" xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar"
xmlns:calendarHelpers="using:Wino.Calendar.Helpers" xmlns:calendarHelpers="using:Wino.Calendar.Helpers"
xmlns:calendarViewModels="using:Wino.Calendar.ViewModels"
xmlns:coreControls="using:Wino.Mail.WinUI.Controls" xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain" xmlns:domain="using:Wino.Core.Domain"
@@ -29,8 +30,10 @@
</Style> </Style>
<Style x:Key="EventDetailsPanelGridStyle" TargetType="Grid"> <Style x:Key="EventDetailsPanelGridStyle" TargetType="Grid">
<Setter Property="Padding" Value="0,12" /> <Setter Property="Padding" Value="12,6" />
<Setter Property="Margin" Value="12,0" /> <Setter Property="Margin" Value="0,12" />
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
</Style> </Style>
<Style <Style
@@ -90,7 +93,7 @@
<AppBarSeparator /> <AppBarSeparator />
<!-- Response Options --> <!-- Response Options -->
<AppBarButton <AppBarButton
Command="{x:Bind ViewModel.RespondCommand}" Command="{x:Bind ViewModel.RespondCommand}"
CommandParameter="{x:Bind enums:CalendarItemStatus.Confirmed}" CommandParameter="{x:Bind enums:CalendarItemStatus.Confirmed}"
@@ -133,17 +136,38 @@
<!-- Reminder --> <!-- Reminder -->
<AppBarElementContainer> <AppBarElementContainer>
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}"> <Button>
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" /> <Button.Content>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_Reminder}" /> <StackPanel Orientation="Horizontal" Spacing="8">
<ComboBox Width="150" /> <coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
</StackPanel> <TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_Reminder}" />
</StackPanel>
</Button.Content>
<Button.Flyout>
<Flyout>
<ListView
Width="200"
MaxHeight="300"
ItemsSource="{x:Bind ViewModel.ReminderOptions}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="calendarViewModels:ReminderOption">
<CheckBox Content="{x:Bind DisplayText}" IsChecked="{x:Bind IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Flyout>
</Button.Flyout>
</Button>
</AppBarElementContainer> </AppBarElementContainer>
<AppBarSeparator /> <AppBarSeparator Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}" />
<!-- Edit Series --> <!-- Edit Series -->
<AppBarButton Label="{x:Bind domain:Translator.CalendarEventDetails_EditSeries}" Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}"> <AppBarButton
Command="{x:Bind ViewModel.ViewSeriesCommand}"
Label="{x:Bind domain:Translator.CalendarEventDetails_EditSeries}"
Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}">
<AppBarButton.Icon> <AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="EventEditSeries" /> <coreControls:WinoFontIcon Icon="EventEditSeries" />
</AppBarButton.Icon> </AppBarButton.Icon>
@@ -153,7 +177,7 @@
<!-- Event details --> <!-- Event details -->
<ScrollViewer Grid.Row="1"> <ScrollViewer Grid.Row="1">
<Grid> <Grid ColumnSpacing="8">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" /> <ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" /> <ColumnDefinition Width="1*" />
@@ -204,7 +228,7 @@
<Grid <Grid
Grid.Row="3" Grid.Row="3"
ColumnSpacing="6" ColumnSpacing="6"
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringParent}"> Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringParent, Mode=OneWay}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@@ -239,7 +263,7 @@
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_People}" /> <TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_People}" />
<Grid Grid.Row="1" RowSpacing="12"> <Grid Grid.Row="1" RowSpacing="6">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -247,10 +271,12 @@
<AutoSuggestBox <AutoSuggestBox
Margin="6,0" Margin="6,0"
BorderThickness="0" BorderThickness="0"
PlaceholderText="{x:Bind domain:Translator.CalendarEventDetails_InviteSomeone}" /> PlaceholderText="{x:Bind domain:Translator.CalendarEventDetails_InviteSomeone}"
Visibility="{x:Bind ViewModel.IsCurrentUserOrganizer, Mode=OneWay}" />
<ListView <ListView
Grid.Row="1" Grid.Row="1"
Margin="-12,0"
IsItemClickEnabled="True" IsItemClickEnabled="True"
ItemsSource="{x:Bind ViewModel.CurrentEvent.Attendees, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.CurrentEvent.Attendees, Mode=OneWay}"
SelectionMode="None"> SelectionMode="None">
+34
View File
@@ -88,6 +88,13 @@
<Content Remove="Assets\Wide310x150Logo.scale-400.png" /> <Content Remove="Assets\Wide310x150Logo.scale-400.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="BackgroundImages\Acrylic.jpg" />
<None Remove="BackgroundImages\Clouds.jpg" />
<None Remove="BackgroundImages\Forest.jpg" />
<None Remove="BackgroundImages\Garden.jpg" />
<None Remove="BackgroundImages\Mica.jpg" />
<None Remove="BackgroundImages\Nighty.jpg" />
<None Remove="BackgroundImages\Snowflake.jpg" />
<None Remove="Controls\ListView\WinoListViewStyles.xaml" /> <None Remove="Controls\ListView\WinoListViewStyles.xaml" />
<None Remove="ShellWindow.xaml" /> <None Remove="ShellWindow.xaml" />
<None Remove="Styles\CalendarThemeResources.xaml" /> <None Remove="Styles\CalendarThemeResources.xaml" />
@@ -102,10 +109,37 @@
<None Remove="Views\Calendar\EventDetailsPage.xaml" /> <None Remove="Views\Calendar\EventDetailsPage.xaml" />
<None Remove="Views\Settings\ContactsPage.xaml" /> <None Remove="Views\Settings\ContactsPage.xaml" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Remove="AppThemes\Acrylic.xaml" />
<Page Remove="AppThemes\Clouds.xaml" />
<Page Remove="AppThemes\Custom.xaml" />
<Page Remove="AppThemes\Default.xaml" />
<Page Remove="AppThemes\Forest.xaml" />
<Page Remove="AppThemes\Garden.xaml" />
<Page Remove="AppThemes\Nighty.xaml" />
<Page Remove="AppThemes\Snowflake.xaml" />
<Page Remove="AppThemes\TestTheme.xaml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="AppThemes\Acrylic.xaml" />
<Content Include="AppThemes\Clouds.xaml" />
<Content Include="AppThemes\Custom.xaml" />
<Content Include="AppThemes\Default.xaml" />
<Content Include="AppThemes\Forest.xaml" />
<Content Include="AppThemes\Garden.xaml" />
<Content Include="AppThemes\Nighty.xaml" />
<Content Include="AppThemes\Snowflake.xaml" />
<Content Include="AppThemes\TestTheme.xaml" />
<Content Include="Assets\LockScreenLogo.scale-200.png" /> <Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Wino_Icon.ico" /> <Content Include="Assets\Wino_Icon.ico" />
<Content Include="BackgroundImages\Acrylic.jpg" />
<Content Include="BackgroundImages\Clouds.jpg" />
<Content Include="BackgroundImages\Forest.jpg" />
<Content Include="BackgroundImages\Garden.jpg" />
<Content Include="BackgroundImages\Mica.jpg" />
<Content Include="BackgroundImages\Nighty.jpg" />
<Content Include="BackgroundImages\Snowflake.jpg" />
<Content Include="JS\editor.html" /> <Content Include="JS\editor.html" />
<Content Include="JS\editor.js" /> <Content Include="JS\editor.js" />
<Content Include="JS\global.css" /> <Content Include="JS\global.css" />
+33
View File
@@ -20,10 +20,15 @@ namespace Wino.Services;
public class CalendarService : BaseDatabaseService, ICalendarService public class CalendarService : BaseDatabaseService, ICalendarService
{ {
// Predefined reminder options in minutes
private static readonly int[] PredefinedReminderMinutes = [60, 30, 15, 5, 1];
public CalendarService(IDatabaseService databaseService) : base(databaseService) public CalendarService(IDatabaseService databaseService) : base(databaseService)
{ {
} }
public int[] GetPredefinedReminderMinutes() => PredefinedReminderMinutes;
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId) public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
=> Connection.Table<AccountCalendar>().Where(x => x.AccountId == accountId).OrderByDescending(a => a.IsPrimary).ToListAsync(); => Connection.Table<AccountCalendar>().Where(x => x.AccountId == accountId).OrderByDescending(a => a.IsPrimary).ToListAsync();
@@ -344,6 +349,10 @@ public class CalendarService : BaseDatabaseService, ICalendarService
return result; return result;
} }
/// <summary>
/// Gets attendees for a calendar item. For recurring event occurrences,
/// callers should pass the EventTrackingId which returns the parent's ID.
/// </summary>
public Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId) public Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId)
=> Connection.Table<CalendarEventAttendee>().Where(x => x.CalendarItemId == calendarEventTrackingId).ToListAsync(); => Connection.Table<CalendarEventAttendee>().Where(x => x.CalendarItemId == calendarEventTrackingId).ToListAsync();
@@ -423,4 +432,28 @@ public class CalendarService : BaseDatabaseService, ICalendarService
} }
} }
} }
/// <summary>
/// Gets reminders for a calendar item. For recurring event occurrences,
/// callers should pass the EventTrackingId which returns the parent's ID.
/// </summary>
public Task<List<Reminder>> GetRemindersAsync(Guid calendarItemId)
=> Connection.Table<Reminder>().Where(r => r.CalendarItemId == calendarItemId).ToListAsync();
public async Task SaveRemindersAsync(Guid calendarItemId, List<Reminder> reminders)
{
await Connection.RunInTransactionAsync((connection) =>
{
// Clear existing reminders for this calendar item
connection.Execute(
"DELETE FROM Reminder WHERE CalendarItemId = ?",
calendarItemId);
// Insert new reminders if any
if (reminders != null && reminders.Count > 0)
{
connection.InsertAll(reminders, typeof(Reminder));
}
});
}
} }