Range thing.

This commit is contained in:
Burak Kaan Köse
2026-03-21 00:58:01 +01:00
parent 01f7a09cb7
commit 51fef043ee
45 changed files with 1327 additions and 3753 deletions
@@ -1,10 +0,0 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Trigger to load more data.
/// </summary>
public enum CalendarInitInitiative
{
User,
App
}
+4 -1
View File
@@ -49,9 +49,12 @@ public interface ICalendarShellClient : IShellClient
IStatePersistanceService StatePersistenceService { get; }
IEnumerable DateNavigationHeaderItems { get; }
int SelectedDateNavigationHeaderIndex { get; }
DateRange? HighlightedDateRange { get; }
VisibleDateRange? CurrentVisibleRange { get; }
string VisibleDateRangeText { get; }
ICommand TodayClickedCommand { get; }
ICommand DateClickedCommand { get; }
ICommand PreviousDateRangeCommand { get; }
ICommand NextDateRangeCommand { get; }
IEnumerable GroupedAccountCalendars { get; }
}
@@ -0,0 +1,6 @@
using System;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Calendar;
public readonly record struct CalendarDisplayRequest(CalendarDisplayType DisplayType, DateOnly AnchorDate);
@@ -0,0 +1,103 @@
using System;
using System.Linq;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Calendar;
public static class CalendarRangeResolver
{
public static VisibleDateRange Resolve(CalendarDisplayRequest request, CalendarSettings settings, DateOnly today)
{
var startDate = GetStartDate(request.DisplayType, request.AnchorDate, settings);
var endDate = GetEndDate(request.DisplayType, request.AnchorDate, startDate, settings);
var dayCount = endDate.DayNumber - startDate.DayNumber + 1;
var dates = Enumerable.Range(0, dayCount)
.Select(offset => startDate.AddDays(offset))
.ToArray();
return new VisibleDateRange(
request.DisplayType,
request.AnchorDate,
startDate,
endDate,
request.AnchorDate,
dayCount,
today >= startDate && today <= endDate,
startDate.Year == endDate.Year && startDate.Month == endDate.Month,
dates);
}
public static VisibleDateRange ChangeDisplayType(VisibleDateRange currentRange, CalendarDisplayType targetDisplayType, CalendarSettings settings, DateOnly today)
{
if (currentRange.DisplayType == targetDisplayType)
{
return currentRange;
}
var anchorDate = currentRange.AnchorDate;
if (currentRange.DisplayType == CalendarDisplayType.Month)
{
anchorDate = currentRange.Contains(today) ? today : currentRange.StartDate;
}
return Resolve(new CalendarDisplayRequest(targetDisplayType, anchorDate), settings, today);
}
public static VisibleDateRange Navigate(VisibleDateRange currentRange, int direction, CalendarSettings settings, DateOnly today)
{
if (direction == 0)
{
return currentRange;
}
var normalizedDirection = Math.Sign(direction);
var anchorDate = currentRange.DisplayType switch
{
CalendarDisplayType.Day => currentRange.AnchorDate.AddDays(normalizedDirection),
CalendarDisplayType.Week => currentRange.AnchorDate.AddDays(7 * normalizedDirection),
CalendarDisplayType.WorkWeek => currentRange.AnchorDate.AddDays(7 * normalizedDirection),
CalendarDisplayType.Month => currentRange.AnchorDate.AddMonths(normalizedDirection),
_ => currentRange.AnchorDate
};
return Resolve(new CalendarDisplayRequest(currentRange.DisplayType, anchorDate), settings, today);
}
private static DateOnly GetStartDate(CalendarDisplayType displayType, DateOnly anchorDate, CalendarSettings settings)
{
return displayType switch
{
CalendarDisplayType.Day => anchorDate,
CalendarDisplayType.Week => GetStartOfWeek(anchorDate, settings.FirstDayOfWeek),
CalendarDisplayType.WorkWeek => GetStartOfWorkWeek(anchorDate, settings),
CalendarDisplayType.Month => new DateOnly(anchorDate.Year, anchorDate.Month, 1),
_ => anchorDate
};
}
private static DateOnly GetEndDate(CalendarDisplayType displayType, DateOnly anchorDate, DateOnly startDate, CalendarSettings settings)
{
return displayType switch
{
CalendarDisplayType.Day => anchorDate,
CalendarDisplayType.Week => startDate.AddDays(6),
CalendarDisplayType.WorkWeek => startDate.AddDays(settings.WorkWeekDayCount - 1),
CalendarDisplayType.Month => new DateOnly(anchorDate.Year, anchorDate.Month, DateTime.DaysInMonth(anchorDate.Year, anchorDate.Month)),
_ => anchorDate
};
}
private static DateOnly GetStartOfWeek(DateOnly date, DayOfWeek firstDayOfWeek)
{
var offset = ((int)date.DayOfWeek - (int)firstDayOfWeek + 7) % 7;
return date.AddDays(-offset);
}
private static DateOnly GetStartOfWorkWeek(DateOnly anchorDate, CalendarSettings settings)
{
var startOfWeek = GetStartOfWeek(anchorDate, settings.FirstDayOfWeek);
var offsetToWorkWeekStart = settings.GetWeekOffset(settings.WorkWeekStart);
return startOfWeek.AddDays(offsetToWorkWeekStart);
}
}
@@ -0,0 +1,24 @@
using System;
using System.Globalization;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Calendar;
public sealed class CalendarRangeTextFormatter : ICalendarRangeTextFormatter
{
public string Format(VisibleDateRange range, IDateContextProvider dateContextProvider)
{
var culture = dateContextProvider.Culture;
var startText = FormatDate(range.StartDate, culture);
if (range.DisplayType == CalendarDisplayType.Day)
{
return startText;
}
return $"{startText} - {FormatDate(range.EndDate, culture)}";
}
private static string FormatDate(DateOnly date, CultureInfo culture)
=> date.ToString(culture.DateTimeFormat.ShortDatePattern, culture);
}
@@ -7,12 +7,33 @@ namespace Wino.Core.Domain.Models.Calendar;
public record CalendarSettings(DayOfWeek FirstDayOfWeek,
List<DayOfWeek> WorkingDays,
DayOfWeek WorkWeekStart,
DayOfWeek WorkWeekEnd,
TimeSpan WorkingHourStart,
TimeSpan WorkingHourEnd,
double HourHeight,
DayHeaderDisplayType DayHeaderDisplayType,
CultureInfo CultureInfo)
{
public int WorkWeekDayCount
{
get
{
var startOffset = GetWeekOffset(WorkWeekStart);
var endOffset = GetWeekOffset(WorkWeekEnd);
if (endOffset < startOffset)
{
endOffset += 7;
}
return (endOffset - startOffset) + 1;
}
}
public int GetWeekOffset(DayOfWeek dayOfWeek)
=> ((int)dayOfWeek - (int)FirstDayOfWeek + 7) % 7;
public TimeSpan? GetTimeSpan(string selectedTime)
{
// Regardless of the format, we need to parse the time to a TimeSpan.
@@ -0,0 +1,6 @@
namespace Wino.Core.Domain.Models.Calendar;
public interface ICalendarRangeTextFormatter
{
string Format(VisibleDateRange range, IDateContextProvider dateContextProvider);
}
@@ -0,0 +1,11 @@
using System;
using System.Globalization;
namespace Wino.Core.Domain.Models.Calendar;
public interface IDateContextProvider
{
CultureInfo Culture { get; }
TimeZoneInfo TimeZone { get; }
DateOnly GetToday();
}
@@ -0,0 +1,17 @@
using System;
using System.Globalization;
namespace Wino.Core.Domain.Models.Calendar;
public sealed class SystemDateContextProvider : IDateContextProvider
{
public CultureInfo Culture => CultureInfo.CurrentCulture;
public TimeZoneInfo TimeZone => TimeZoneInfo.Local;
public DateOnly GetToday()
{
var localNow = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, TimeZone);
return DateOnly.FromDateTime(localNow.DateTime);
}
}
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Itenso.TimePeriod;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Calendar;
public sealed record VisibleDateRange(
CalendarDisplayType DisplayType,
DateOnly AnchorDate,
DateOnly StartDate,
DateOnly EndDate,
DateOnly PrimaryDate,
int DayCount,
bool ContainsToday,
bool SpansSingleMonth,
IReadOnlyList<DateOnly> Dates)
{
public DateRange ToDateRangeExclusive()
=> new(StartDate.ToDateTime(TimeOnly.MinValue), EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue));
public ITimePeriod ToTimePeriod()
=> new TimeRange(StartDate.ToDateTime(TimeOnly.MinValue), EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue));
public bool Contains(DateOnly date)
=> date >= StartDate && date <= EndDate;
public bool Contains(DateTime date)
=> Contains(DateOnly.FromDateTime(date));
public static VisibleDateRange FromDateRange(CalendarDisplayType displayType, DateRange dateRange, DateOnly anchorDate, DateOnly today)
{
var startDate = DateOnly.FromDateTime(dateRange.StartDate);
var endDate = DateOnly.FromDateTime(dateRange.EndDate.AddDays(-1));
var dayCount = endDate.DayNumber - startDate.DayNumber + 1;
var dates = Enumerable.Range(0, dayCount)
.Select(offset => startDate.AddDays(offset))
.ToArray();
return new VisibleDateRange(
displayType,
anchorDate,
startDate,
endDate,
anchorDate,
dayCount,
today >= startDate && today <= endDate,
startDate.Year == endDate.Year && startDate.Month == endDate.Month,
dates);
}
}
@@ -40,7 +40,7 @@ public static class SettingsNavigationInfoProvider
Translator.WinoAccount_SettingsSection_Title,
Translator.WinoAccount_SettingsSection_Description,
"\uE77B",
searchKeywords: Translator.SettingsSearch_WinoAccount_Keywords),
searchKeywords: string.Empty),
new(null, Translator.SettingsOptions_GeneralSection, string.Empty, "\uE713", isSeparator: true),
new(WinoPage.AppPreferencesPage,
Translator.SettingsAppPreferences_Title,
@@ -18,6 +18,7 @@
"AccountCreationDialog_Initializing": "initializing",
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
"AccountCreationDialog_SigninIn": "Account information is being saved.",
"Purchased": "Purchased",
"AccountEditDialog_Message": "Account Name",
"AccountEditDialog_Title": "Edit Account",
"AccountPickerDialog_Title": "Pick an account",
@@ -1146,6 +1147,56 @@
"WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later",
"WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11",
"WelcomeWizard_Step1Title": "Welcome",
"SystemTrayMenu_Open": "Open",
"WinoAccount_Titlebar_SyncBenefitTitle": "Sync settings",
"WinoAccount_Titlebar_SyncBenefitDescription": "Keep your Wino preferences in sync across devices.",
"WinoAccount_Titlebar_AddonsBenefitTitle": "Unlock add-ons",
"WinoAccount_Titlebar_AddonsBenefitDescription": "Access premium features like Wino AI Pack.",
"WinoAccount_Management_Description": "Manage your Wino Account, AI Pack access, and synchronized settings.",
"WinoAccount_Management_SignedOutTitle": "Sign in to Wino Mail",
"WinoAccount_Management_SignedOutDescription": "Sign in or create an account to sync your email, access AI features, and manage your settings across devices.",
"WinoAccount_Management_ProfileSectionHeader": "Profile",
"WinoAccount_Management_AddOnsSectionHeader": "Wino Add-Ons",
"WinoAccount_Management_DataSectionHeader": "Data",
"WinoAccount_Management_AccountActionsSectionHeader": "Account actions",
"WinoAccount_Management_AccountCardTitle": "Account",
"WinoAccount_Management_AccountCardDescription": "Your Wino Account email address and current account state.",
"WinoAccount_Management_AiPackCardTitle": "AI Pack",
"WinoAccount_Management_AiPackCardDescription": "See whether Wino AI Pack is active and how much usage is left.",
"WinoAccount_Management_AiPackActive": "AI Pack is active",
"WinoAccount_Management_AiPackInactive": "AI Pack is not active",
"WinoAccount_Management_AiPackUsage": "{0} of {1} uses consumed. {2} remaining.",
"WinoAccount_Management_AiPackBillingPeriod": "Billing period: {0:d} - {1:d}",
"WinoAccount_Management_AiPackUnknownUsage": "Usage details are not available yet.",
"WinoAccount_Management_AiPackBuyDescription": "Buy Wino AI Pack to translate, rewrite or summarize emails with AI.",
"WinoAccount_Management_AiPackPromoTitle": "Unlock AI Pack",
"WinoAccount_Management_AiPackPromoDescription": "Supercharge your email workflow with AI-powered tools. Translate messages into 50+ languages, rewrite for clarity and tone, and get instant summaries of long threads.",
"WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo",
"WinoAccount_Management_AiPackPromoRequests": "1,000 requests",
"WinoAccount_Management_AiPackGetButton": "Get AI Pack",
"WinoAccount_Management_PurchaseRequiresSignIn": "Sign in with your Wino Account to complete this purchase.",
"WinoAccount_Management_PurchaseStartFailed": "Wino could not start the checkout session for this add-on.",
"WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active",
"WinoAccount_Management_AiPackRenews": "Renews {0:d}",
"WinoAccount_Management_AiPackRequestsUsed": "Requests used this month",
"WinoAccount_Management_AiPackResets": "Resets {0:d}",
"WinoAccount_Management_AiPackFeatureTranslate": "Translate",
"WinoAccount_Management_AiPackFeatureRewrite": "Rewrite",
"WinoAccount_Management_AiPackFeatureSummarize": "Summarize",
"WinoAccount_Management_SyncPreferencesTitle": "Synchronize Preferences",
"WinoAccount_Management_SyncPreferencesDescription": "Import or export your preferences to cloud. Import them across devices.",
"WinoAccount_Management_SignOutTitle": "Sign out",
"WinoAccount_Management_SignOutDescription": "Sign out of your account on this device",
"WinoAccount_Management_StatusLabel": "Status: {0}",
"WinoAccount_Management_NoRemoteSettings": "There are no synchronized settings stored for this account yet.",
"WinoAccount_Management_ExportSucceeded": "Your settings were exported to your Wino Account.",
"WinoAccount_Management_ImportSucceeded": "Imported {0} settings from your Wino Account.",
"WinoAccount_Management_ImportPartial": "Imported {0} settings. {1} settings could not be restored.",
"WinoAccount_Management_SerializeFailed": "Wino could not serialize your current preferences.",
"WinoAccount_Management_EmptyExport": "There are no preference values to export.",
"WinoAccount_Management_ImportEmpty": "The synchronized settings payload does not contain any values to restore.",
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
"WinoAccount_Management_ActionFailed": "The Wino Account request could not be completed.",
"WinoAccount_SettingsSection_Title": "Wino Account",
"WinoAccount_SettingsSection_Description": "Create or sign in to a Wino Account using your localhost auth service.",
"WinoAccount_RegisterButton_Title": "Register account",
@@ -1168,6 +1219,10 @@
"WinoAccount_RegisterDialog_DifferenceTitle": "Wino Account is separate from your mail accounts",
"WinoAccount_RegisterDialog_DifferenceDescription": "Your Outlook, Gmail, IMAP, or other email accounts stay exactly as they are. A Wino Account only manages Wino-specific features and account-based add-ons.",
"WinoAccount_RegisterDialog_PrimaryButton": "Register",
"WinoAccount_RegisterDialog_PrivacyTitle": "Privacy and API processing",
"WinoAccount_RegisterDialog_PrivacyDescription": "Optional add-ons such as Wino AI Pack may send selected email HTML content to the Wino API service only when you use those features.",
"WinoAccount_RegisterDialog_PrivacyLinkText": "Read the privacy policy",
"WinoAccount_RegisterDialog_PrivacyCheckbox": "I agree to the privacy policy.",
"WinoAccount_LoginDialog_Title": "Sign In to Wino Account",
"WinoAccount_LoginDialog_Description": "Sign in to your Wino Account to sync your Wino setup and access account-based features.",
"WinoAccount_LoginDialog_HeroTitle": "Welcome back",
@@ -1175,17 +1230,28 @@
"WinoAccount_LoginDialog_BenefitsDescription": "Use your Wino Account to continue syncing settings across devices and to access paid add-ons such as Wino AI Pack.",
"WinoAccount_LoginDialog_DifferenceTitle": "This is not your email mailbox sign-in",
"WinoAccount_LoginDialog_DifferenceDescription": "Signing in here does not add or replace your Outlook, Gmail, or IMAP accounts in Wino. It only signs you in to Wino-specific services.",
"WinoAccount_LoginDialog_ForgotPasswordLink": "Forgot password?",
"WinoAccount_EmailLabel": "Email",
"WinoAccount_EmailPlaceholder": "name@example.com",
"WinoAccount_PasswordLabel": "Password",
"WinoAccount_ConfirmPasswordLabel": "Confirm password",
"WinoAccount_ForgotPasswordDialog_Title": "Reset your password",
"WinoAccount_ForgotPasswordDialog_PrimaryButton": "Send reset email",
"WinoAccount_ForgotPasswordDialog_BackToSignIn": "Back to sign in",
"WinoAccount_ForgotPasswordDialog_Description": "Enter your Wino Account email address and we will send you a password reset link if the address is registered.",
"WinoAccount_Validation_EmailRequired": "Email is required.",
"WinoAccount_Validation_PasswordRequired": "Password is required.",
"WinoAccount_Validation_PasswordMismatch": "Passwords do not match.",
"WinoAccount_Validation_PrivacyConsentRequired": "You must accept the privacy policy before creating a Wino Account.",
"WinoAccount_Error_InvalidCredentials": "The email address or password is incorrect.",
"WinoAccount_Error_AccountLocked": "This account is temporarily locked.",
"WinoAccount_Error_AccountBanned": "This account has been banned.",
"WinoAccount_Error_AccountSuspended": "This account has been suspended.",
"WinoAccount_Error_EmailNotConfirmed": "Please confirm your email address before signing in.",
"WinoAccount_Error_EmailConfirmationRequired": "Please confirm your email address before signing in.",
"WinoAccount_Error_EmailConfirmationResendNotAvailable": "A new confirmation email is not available yet.",
"WinoAccount_Error_EmailConfirmationResendInvalid": "This confirmation request is no longer valid. Please try signing in again.",
"WinoAccount_Error_EmailNotRegistered": "This email address is not registered.",
"WinoAccount_Error_RefreshTokenInvalid": "Your session is no longer valid. Please sign in again.",
"WinoAccount_Error_EmailAlreadyRegistered": "This email address is already registered.",
"WinoAccount_Error_ExternalLoginEmailRequired": "An email address is required to complete external sign-in.",
@@ -1196,14 +1262,25 @@
"WinoAccount_Error_ValidationFailed": "The request is invalid. Please review the entered values.",
"WinoAccount_RegisterSuccessMessage": "Wino Account registration completed for {0}.",
"WinoAccount_LoginSuccessMessage": "Signed in to Wino Account as {0}.",
"WinoAccount_EmailConfirmationSentDialog_Title": "Confirm your email address",
"WinoAccount_EmailConfirmationSentDialog_Message": "We sent an email confirmation to {0}. Please confirm it and try signing in again.",
"WinoAccount_EmailConfirmationPendingDialog_Title": "Email confirmation required",
"WinoAccount_EmailConfirmationPendingDialog_Message": "We are still waiting for you to confirm {0}.",
"WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Resend confirmation email",
"WinoAccount_EmailConfirmationPendingDialog_Countdown": "You can resend the confirmation email in {0}.",
"WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "You can resend the confirmation email now.",
"WinoAccount_EmailConfirmationResentDialog_Title": "Confirmation email resent",
"WinoAccount_EmailConfirmationResentDialog_Message": "We sent another confirmation email to {0}. Please confirm it and try signing in again.",
"WinoAccount_ForgotPasswordDialog_SuccessTitle": "Password reset email sent",
"WinoAccount_ForgotPasswordDialog_SuccessMessage": "We sent a password reset email to {0}. Open that message to choose a new password.",
"WinoAccount_ChangePassword_Title": "Change password",
"WinoAccount_ChangePassword_Description": "Send a password reset email to this Wino Account.",
"WinoAccount_ChangePassword_Action": "Send reset email",
"WinoAccount_ChangePassword_ConfirmationMessage": "Do you want Wino to send a password reset email to {0}?",
"WinoAccount_SignOut_SuccessMessage": "Signed out from Wino Account {0}.",
"WinoAccount_SignOut_NoAccountMessage": "There is no active Wino Account to sign out.",
"WinoAccount_Titlebar_SignedOutTitle": "Wino Account",
"WinoAccount_Titlebar_SignedOutDescription": "Sign in or create a Wino Account to manage your Wino session.",
"WinoAccount_Titlebar_SyncBenefitTitle": "Sync settings",
"WinoAccount_Titlebar_SyncBenefitDescription": "Keep your Wino preferences in sync across devices.",
"WinoAccount_Titlebar_AddonsBenefitTitle": "Unlock add-ons",
"WinoAccount_Titlebar_AddonsBenefitDescription": "Access premium features like Wino AI Pack.",
"WinoAccount_Titlebar_SignedInStatus": "Status: {0}",
"WelcomeWizard_Step2Title": "Add Account",
"WelcomeWizard_Step3Title": "Finish Setup",