2026-02-11 14:50:59 +01:00
|
|
|
using System.Collections.Generic;
|
2026-03-20 12:43:09 +01:00
|
|
|
using System;
|
2026-02-22 15:13:39 +01:00
|
|
|
using System.IO;
|
2025-11-14 11:37:26 +01:00
|
|
|
using System.Linq;
|
2025-09-29 11:16:14 +02:00
|
|
|
using System.Text;
|
2026-02-11 14:50:59 +01:00
|
|
|
using System.Threading;
|
2025-11-14 11:37:26 +01:00
|
|
|
using System.Threading.Tasks;
|
2025-09-29 11:16:14 +02:00
|
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2025-11-12 15:44:43 +01:00
|
|
|
using Microsoft.Toolkit.Uwp.Notifications;
|
2026-03-20 12:43:09 +01:00
|
|
|
using Microsoft.UI.Dispatching;
|
2025-11-14 11:37:26 +01:00
|
|
|
using Microsoft.UI.Xaml;
|
2026-03-27 14:45:36 +01:00
|
|
|
using Microsoft.UI.Xaml.Media.Animation;
|
2025-10-21 15:40:19 +02:00
|
|
|
using Microsoft.Windows.AppLifecycle;
|
2025-11-14 11:37:26 +01:00
|
|
|
using Microsoft.Windows.AppNotifications;
|
2025-11-23 20:56:57 +01:00
|
|
|
using MimeKit.Cryptography;
|
2026-02-22 15:13:39 +01:00
|
|
|
using Windows.ApplicationModel.Activation;
|
2025-12-26 20:46:48 +01:00
|
|
|
using Wino.Calendar.ViewModels;
|
|
|
|
|
using Wino.Calendar.ViewModels.Interfaces;
|
|
|
|
|
using Wino.Core;
|
2025-11-12 15:44:43 +01:00
|
|
|
using Wino.Core.Domain;
|
2025-10-03 21:04:23 +02:00
|
|
|
using Wino.Core.Domain.Enums;
|
2025-09-29 11:16:14 +02:00
|
|
|
using Wino.Core.Domain.Interfaces;
|
2026-02-11 01:49:29 +01:00
|
|
|
using Wino.Core.Domain.Models.Calendar;
|
2025-11-12 18:51:53 +01:00
|
|
|
using Wino.Core.Domain.Models.MailItem;
|
2026-03-05 10:12:03 +01:00
|
|
|
using Wino.Core.Domain.Models.Navigation;
|
2025-11-14 11:37:26 +01:00
|
|
|
using Wino.Core.Domain.Models.Synchronization;
|
2026-03-18 09:00:26 +01:00
|
|
|
using Wino.Core.ViewModels;
|
2025-09-29 11:16:14 +02:00
|
|
|
using Wino.Mail.Services;
|
|
|
|
|
using Wino.Mail.ViewModels;
|
2026-03-06 03:42:08 +01:00
|
|
|
using Wino.Mail.ViewModels.Data;
|
2026-02-22 15:13:39 +01:00
|
|
|
using Wino.Mail.WinUI.Activation;
|
2025-11-23 20:56:57 +01:00
|
|
|
using Wino.Mail.WinUI.Interfaces;
|
2026-03-05 10:12:03 +01:00
|
|
|
using Wino.Mail.WinUI.Models;
|
2025-12-26 20:46:48 +01:00
|
|
|
using Wino.Mail.WinUI.Services;
|
2026-03-18 09:00:26 +01:00
|
|
|
using Wino.Mail.WinUI.ViewModels;
|
2025-11-12 15:44:43 +01:00
|
|
|
using Wino.Messaging.Client.Accounts;
|
2026-03-05 10:12:03 +01:00
|
|
|
using Wino.Messaging.Client.Navigation;
|
2025-09-29 11:16:14 +02:00
|
|
|
using Wino.Messaging.Server;
|
2026-02-22 15:13:39 +01:00
|
|
|
using Wino.Messaging.UI;
|
2025-09-29 11:16:14 +02:00
|
|
|
using Wino.Services;
|
2026-03-09 14:18:13 +01:00
|
|
|
using Wino.Views;
|
2026-03-01 09:47:05 +01:00
|
|
|
using WinUIEx;
|
2025-09-29 11:16:14 +02:00
|
|
|
namespace Wino.Mail.WinUI;
|
|
|
|
|
|
2025-12-26 20:46:48 +01:00
|
|
|
public partial class App : WinoApplication,
|
|
|
|
|
IRecipient<NewMailSynchronizationRequested>,
|
2026-03-05 10:12:03 +01:00
|
|
|
IRecipient<NewCalendarSynchronizationRequested>,
|
|
|
|
|
IRecipient<AccountCreatedMessage>,
|
2026-03-06 03:42:08 +01:00
|
|
|
IRecipient<AccountRemovedMessage>,
|
2026-03-05 10:12:03 +01:00
|
|
|
IRecipient<GetStartedFromWelcomeRequested>
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-11 14:50:59 +01:00
|
|
|
private const int InboxSyncsPerFullSync = 20;
|
2026-02-27 11:00:25 +01:00
|
|
|
private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default";
|
2026-03-19 10:26:17 +01:00
|
|
|
private const string WinoProtocolScheme = "wino";
|
|
|
|
|
private const string BillingProtocolHost = "billing";
|
|
|
|
|
private const string BillingSuccessPath = "/success";
|
2025-10-21 01:27:29 +02:00
|
|
|
private ISynchronizationManager? _synchronizationManager;
|
2026-02-11 14:50:59 +01:00
|
|
|
private IPreferencesService? _preferencesService;
|
|
|
|
|
private IAccountService? _accountService;
|
2026-03-05 10:12:03 +01:00
|
|
|
private bool _windowManagerConfigured;
|
2026-03-20 12:43:09 +01:00
|
|
|
private bool _hasConfiguredAccounts;
|
|
|
|
|
private bool _isExiting;
|
2026-02-11 14:50:59 +01:00
|
|
|
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
|
|
|
|
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
|
|
|
|
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
|
2026-03-20 12:43:09 +01:00
|
|
|
private NativeTrayIcon? _trayIcon;
|
|
|
|
|
|
|
|
|
|
internal bool IsExiting => _isExiting;
|
2025-10-12 16:23:33 +02:00
|
|
|
|
2025-09-29 11:16:14 +02:00
|
|
|
public App()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
|
|
|
|
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
2025-11-23 20:56:57 +01:00
|
|
|
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2025-10-21 01:27:29 +02:00
|
|
|
RegisterRecipients();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
private void EnsureWindowManagerConfigured()
|
|
|
|
|
{
|
|
|
|
|
if (_windowManagerConfigured)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
windowManager.ActiveWindowChanged -= OnActiveWindowChanged;
|
|
|
|
|
windowManager.ActiveWindowChanged += OnActiveWindowChanged;
|
|
|
|
|
windowManager.WindowRemoved -= OnManagedWindowRemoved;
|
|
|
|
|
windowManager.WindowRemoved += OnManagedWindowRemoved;
|
|
|
|
|
|
|
|
|
|
var nativeAppService = Services.GetRequiredService<INativeAppService>();
|
|
|
|
|
nativeAppService.GetCoreWindowHwnd = () =>
|
|
|
|
|
{
|
|
|
|
|
var window = windowManager.ActiveWindow
|
|
|
|
|
?? windowManager.GetWindow(WinoWindowKind.Shell)
|
|
|
|
|
?? windowManager.GetWindow(WinoWindowKind.Welcome)
|
|
|
|
|
?? MainWindow;
|
|
|
|
|
|
|
|
|
|
return window == null
|
|
|
|
|
? IntPtr.Zero
|
|
|
|
|
: WinRT.Interop.WindowNative.GetWindowHandle(window);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_windowManagerConfigured = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnActiveWindowChanged(object? sender, WindowEx? window)
|
|
|
|
|
{
|
|
|
|
|
if (window == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
MainWindow = window;
|
|
|
|
|
InitializeNavigationDispatcher();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnManagedWindowRemoved(object? sender, WindowEx window)
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
MainWindow = windowManager.ActiveWindow
|
|
|
|
|
?? windowManager.GetWindow(WinoWindowKind.Shell)
|
|
|
|
|
?? windowManager.GetWindow(WinoWindowKind.Welcome);
|
|
|
|
|
|
|
|
|
|
InitializeNavigationDispatcher();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:43:09 +01:00
|
|
|
private void EnsureTrayIconCreated()
|
|
|
|
|
{
|
|
|
|
|
if (_trayIcon != null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "Wino_Icon.ico");
|
|
|
|
|
var dispatcherQueue = DispatcherQueue.GetForCurrentThread()
|
|
|
|
|
?? throw new InvalidOperationException("Tray icon must be created on a thread with a DispatcherQueue.");
|
|
|
|
|
|
|
|
|
|
_trayIcon = new NativeTrayIcon(
|
|
|
|
|
dispatcherQueue,
|
|
|
|
|
iconPath,
|
|
|
|
|
"Wino Mail",
|
|
|
|
|
BuildTrayMenu,
|
|
|
|
|
ActivatePreferredWindowAsync);
|
|
|
|
|
|
|
|
|
|
_trayIcon.Create();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IReadOnlyList<NativeTrayIcon.NativeTrayMenuItem> BuildTrayMenu()
|
|
|
|
|
{
|
|
|
|
|
List<NativeTrayIcon.NativeTrayMenuItem> items =
|
|
|
|
|
[
|
|
|
|
|
new(Translator.SystemTrayMenu_Open, ActivatePreferredWindowAsync, IsDefault: true),
|
|
|
|
|
new(Translator.SystemTrayMenu_ShowWino, OpenMailFromTrayAsync)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
items.Add(new NativeTrayIcon.NativeTrayMenuItem(
|
|
|
|
|
Translator.SystemTrayMenu_ShowWinoCalendar,
|
|
|
|
|
OpenCalendarFromTrayAsync));
|
|
|
|
|
items.Add(new NativeTrayIcon.NativeTrayMenuItem(
|
|
|
|
|
Translator.SystemTrayMenu_ExitWino,
|
|
|
|
|
ExitApplicationAsync));
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Task ActivatePreferredWindowAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!_hasConfiguredAccounts)
|
|
|
|
|
return ActivateWelcomeWindowAsync();
|
|
|
|
|
|
|
|
|
|
return ActivateShellWindowAsync(_preferencesService?.DefaultApplicationMode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Task OpenMailFromTrayAsync()
|
|
|
|
|
=> _hasConfiguredAccounts
|
|
|
|
|
? ActivateShellWindowAsync(WinoApplicationMode.Mail)
|
|
|
|
|
: ActivateWelcomeWindowAsync();
|
|
|
|
|
|
|
|
|
|
private Task OpenCalendarFromTrayAsync()
|
|
|
|
|
=> _hasConfiguredAccounts
|
|
|
|
|
? ActivateShellWindowAsync(WinoApplicationMode.Calendar)
|
|
|
|
|
: ActivateWelcomeWindowAsync();
|
|
|
|
|
|
|
|
|
|
private async Task ActivateWelcomeWindowAsync()
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
var welcomeWindow = windowManager.GetWindow(WinoWindowKind.Welcome) as WelcomeWindow;
|
|
|
|
|
|
|
|
|
|
if (welcomeWindow == null)
|
|
|
|
|
{
|
|
|
|
|
CreateWelcomeWindow();
|
|
|
|
|
welcomeWindow = MainWindow as WelcomeWindow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (welcomeWindow == null)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-03-27 14:45:36 +01:00
|
|
|
CloseShellWindowIfPresent();
|
2026-03-20 12:43:09 +01:00
|
|
|
await ActivateWindowAsync(welcomeWindow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ActivateShellWindowAsync(WinoApplicationMode? mode, IWinoShellWindow? existingShellWindow = null)
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
var shellWindow = existingShellWindow;
|
|
|
|
|
|
|
|
|
|
if (shellWindow == null)
|
|
|
|
|
{
|
|
|
|
|
shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow;
|
|
|
|
|
|
|
|
|
|
if (shellWindow == null)
|
|
|
|
|
{
|
|
|
|
|
CreateWindow(null);
|
|
|
|
|
shellWindow = MainWindow as IWinoShellWindow;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shellWindow == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (mode.HasValue)
|
|
|
|
|
shellWindow.HandleAppActivation(GetModeLaunchArgument(mode.Value));
|
|
|
|
|
|
|
|
|
|
CloseWelcomeWindowIfPresent();
|
|
|
|
|
await ActivateWindowAsync((WindowEx)shellWindow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CloseWelcomeWindowIfPresent()
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
if (windowManager.GetWindow(WinoWindowKind.Welcome) is not WelcomeWindow welcomeWindow)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
welcomeWindow.AllowClose();
|
|
|
|
|
welcomeWindow.Close();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 14:45:36 +01:00
|
|
|
private void CloseShellWindowIfPresent()
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
if (windowManager.GetWindow(WinoWindowKind.Shell) is not ShellWindow shellWindow)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
shellWindow.PrepareForClose();
|
|
|
|
|
shellWindow.Close();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:43:09 +01:00
|
|
|
private async Task ActivateWindowAsync(WindowEx window)
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
MainWindow = window;
|
|
|
|
|
windowManager.ActivateWindow(window);
|
|
|
|
|
await NewThemeService.ApplyThemeToActiveWindowAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Task ExitApplicationAsync()
|
|
|
|
|
{
|
|
|
|
|
ExitApplication();
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ExitApplication()
|
|
|
|
|
{
|
|
|
|
|
if (_isExiting)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_isExiting = true;
|
|
|
|
|
_trayIcon?.Dispose();
|
|
|
|
|
_trayIcon = null;
|
|
|
|
|
|
|
|
|
|
Services.GetRequiredService<IWinoWindowManager>().CloseAllWindows();
|
|
|
|
|
Application.Current.Exit();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args)
|
2025-11-12 15:44:43 +01:00
|
|
|
{
|
2025-11-14 11:37:26 +01:00
|
|
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
2025-11-12 15:44:43 +01:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
if (activationArgs.Kind == ExtendedActivationKind.AppNotification)
|
2025-11-12 15:44:43 +01:00
|
|
|
{
|
2025-11-14 11:37:26 +01:00
|
|
|
args = ((AppNotificationActivatedEventArgs)activationArgs.Data);
|
|
|
|
|
return true;
|
2025-11-12 15:44:43 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
args = null!;
|
|
|
|
|
return false;
|
2025-11-12 15:44:43 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 11:16:14 +02:00
|
|
|
#region Dependency Injection
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void RegisterUWPServices(IServiceCollection services)
|
|
|
|
|
{
|
|
|
|
|
services.AddSingleton<INavigationService, NavigationService>();
|
|
|
|
|
services.AddSingleton<IMailDialogService, DialogService>();
|
|
|
|
|
services.AddTransient<IProviderService, ProviderService>();
|
|
|
|
|
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
2025-12-26 20:46:48 +01:00
|
|
|
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
|
2026-03-21 00:58:01 +01:00
|
|
|
services.AddSingleton<IDateContextProvider, SystemDateContextProvider>();
|
|
|
|
|
services.AddSingleton<ICalendarRangeTextFormatter, CalendarRangeTextFormatter>();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RegisterViewModels(IServiceCollection services)
|
|
|
|
|
{
|
2025-12-26 20:46:48 +01:00
|
|
|
services.AddSingleton(typeof(MailAppShellViewModel));
|
|
|
|
|
services.AddSingleton(typeof(CalendarAppShellViewModel));
|
2026-03-11 01:39:32 +01:00
|
|
|
services.AddSingleton(typeof(ContactsShellClient));
|
2026-03-12 19:04:47 +01:00
|
|
|
services.AddSingleton(typeof(SettingsShellClient));
|
2026-03-11 01:39:32 +01:00
|
|
|
services.AddSingleton(typeof(WinoAppShellViewModel));
|
|
|
|
|
services.AddSingleton<IMailShellClient>(serviceProvider => serviceProvider.GetRequiredService<MailAppShellViewModel>());
|
|
|
|
|
services.AddSingleton<ICalendarShellClient>(serviceProvider => serviceProvider.GetRequiredService<CalendarAppShellViewModel>());
|
|
|
|
|
services.AddSingleton<IShellClient>(serviceProvider => serviceProvider.GetRequiredService<MailAppShellViewModel>());
|
|
|
|
|
services.AddSingleton<IShellClient>(serviceProvider => serviceProvider.GetRequiredService<CalendarAppShellViewModel>());
|
|
|
|
|
services.AddSingleton<IShellClient>(serviceProvider => serviceProvider.GetRequiredService<ContactsShellClient>());
|
2026-03-12 19:04:47 +01:00
|
|
|
services.AddSingleton<IShellClient>(serviceProvider => serviceProvider.GetRequiredService<SettingsShellClient>());
|
2025-09-29 11:16:14 +02:00
|
|
|
|
|
|
|
|
services.AddTransient(typeof(MailListPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(MailRenderingPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(AccountManagementViewModel));
|
2026-03-05 10:12:03 +01:00
|
|
|
services.AddTransient(typeof(WelcomePageV2ViewModel));
|
2026-03-06 03:42:08 +01:00
|
|
|
services.AddTransient(typeof(ProviderSelectionPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(AccountSetupProgressPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(SpecialImapCredentialsPageViewModel));
|
|
|
|
|
services.AddSingleton(typeof(WelcomeWizardContext));
|
2025-09-29 11:16:14 +02:00
|
|
|
|
|
|
|
|
services.AddTransient(typeof(ComposePageViewModel));
|
|
|
|
|
services.AddTransient(typeof(IdlePageViewModel));
|
|
|
|
|
|
2026-02-15 02:20:18 +01:00
|
|
|
services.AddTransient(typeof(ImapCalDavSettingsPageViewModel));
|
2025-09-29 11:16:14 +02:00
|
|
|
services.AddTransient(typeof(AccountDetailsPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(SignatureManagementPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(MessageListPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(ReadComposePanePageViewModel));
|
|
|
|
|
services.AddTransient(typeof(MergedAccountDetailsPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(AppPreferencesPageViewModel));
|
2026-03-08 16:25:53 +01:00
|
|
|
services.AddTransient(typeof(StoragePageViewModel));
|
2026-03-18 09:00:26 +01:00
|
|
|
services.AddTransient(typeof(WinoAccountManagementPageViewModel));
|
2025-09-29 11:16:14 +02:00
|
|
|
services.AddTransient(typeof(AliasManagementPageViewModel));
|
2025-10-29 19:35:04 +01:00
|
|
|
services.AddTransient(typeof(ContactsPageViewModel));
|
2025-11-23 20:56:57 +01:00
|
|
|
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
|
2026-03-08 15:48:11 +01:00
|
|
|
services.AddTransient(typeof(EmailTemplatesPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(CreateEmailTemplatePageViewModel));
|
2026-03-11 19:26:37 +01:00
|
|
|
services.AddSingleton(typeof(CalendarPageViewModel));
|
2026-03-24 01:18:06 +01:00
|
|
|
services.AddTransient(typeof(CalendarRenderingSettingsPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(CalendarNotificationSettingsPageViewModel));
|
|
|
|
|
services.AddTransient(typeof(CalendarPreferenceSettingsPageViewModel));
|
2026-01-06 17:23:58 +01:00
|
|
|
services.AddTransient(typeof(CalendarAccountSettingsPageViewModel));
|
2025-12-26 20:46:48 +01:00
|
|
|
services.AddTransient(typeof(EventDetailsPageViewModel));
|
2026-03-06 17:46:38 +01:00
|
|
|
services.AddTransient(typeof(CalendarEventComposePageViewModel));
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
public override IServiceProvider ConfigureServices()
|
|
|
|
|
{
|
|
|
|
|
var services = new ServiceCollection();
|
|
|
|
|
|
2025-12-26 20:46:48 +01:00
|
|
|
services.RegisterCoreServices();
|
2025-09-29 11:16:14 +02:00
|
|
|
services.RegisterSharedServices();
|
|
|
|
|
services.RegisterCoreUWPServices();
|
|
|
|
|
services.RegisterCoreViewModels();
|
|
|
|
|
|
|
|
|
|
RegisterUWPServices(services);
|
|
|
|
|
RegisterViewModels(services);
|
|
|
|
|
|
|
|
|
|
return services.BuildServiceProvider();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 15:40:19 +02:00
|
|
|
private bool IsStartupTaskLaunch() => AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask;
|
2025-11-12 15:44:43 +01:00
|
|
|
public bool IsAppRunning() => MainWindow != null;
|
2025-10-21 15:40:19 +02:00
|
|
|
|
2025-09-29 11:16:14 +02:00
|
|
|
protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
|
|
|
|
{
|
2025-11-14 11:37:26 +01:00
|
|
|
base.OnLaunched(args);
|
2025-11-12 15:44:43 +01:00
|
|
|
|
2026-02-27 11:00:25 +01:00
|
|
|
// Always register notification callbacks.
|
2026-02-25 01:36:26 +01:00
|
|
|
TryRegisterAppNotifications();
|
2025-11-14 11:37:26 +01:00
|
|
|
|
|
|
|
|
// Initialize required services regardless of launch activation type.
|
|
|
|
|
// All activation scenarios require these services to be ready.
|
|
|
|
|
// Note: Theme service is initialized separately after window creation.
|
|
|
|
|
await InitializeServicesAsync();
|
2025-10-03 21:04:23 +02:00
|
|
|
|
2025-10-12 16:23:33 +02:00
|
|
|
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
|
2026-02-11 14:50:59 +01:00
|
|
|
_preferencesService = Services.GetRequiredService<IPreferencesService>();
|
|
|
|
|
_accountService = Services.GetRequiredService<IAccountService>();
|
2026-03-08 18:40:43 +01:00
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
EnsureWindowManagerConfigured();
|
2026-03-20 12:43:09 +01:00
|
|
|
EnsureTrayIconCreated();
|
2026-03-05 10:12:03 +01:00
|
|
|
|
|
|
|
|
var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any();
|
2026-03-20 12:43:09 +01:00
|
|
|
_hasConfiguredAccounts = hasAnyAccount;
|
2026-03-05 10:12:03 +01:00
|
|
|
if (!IsStartupTaskLaunch() && !hasAnyAccount)
|
|
|
|
|
{
|
|
|
|
|
CreateWelcomeWindow();
|
|
|
|
|
await NewThemeService.InitializeAsync();
|
|
|
|
|
MainWindow?.Activate();
|
|
|
|
|
LogActivation("Welcome window created and activated.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-11 14:50:59 +01:00
|
|
|
|
|
|
|
|
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
|
|
|
|
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
2026-03-02 00:44:29 +01:00
|
|
|
|
2026-02-11 14:50:59 +01:00
|
|
|
RestartAutoSynchronizationLoop();
|
2025-10-12 16:23:33 +02:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
// Check if launched from toast notification.
|
|
|
|
|
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
|
|
|
|
{
|
|
|
|
|
await HandleToastActivationAsync(toastArgs);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-03 21:04:23 +02:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
// Check if launched by startup task.
|
|
|
|
|
bool isStartupTaskLaunch = IsStartupTaskLaunch();
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-03-20 12:43:09 +01:00
|
|
|
if (isStartupTaskLaunch && !hasAnyAccount)
|
|
|
|
|
{
|
|
|
|
|
CreateWelcomeWindow();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
CreateWindow(args);
|
|
|
|
|
}
|
2025-10-06 17:46:00 +02:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
// Initialize theme service after window creation.
|
|
|
|
|
// Theme service requires the window to exist to properly load and apply themes.
|
|
|
|
|
await NewThemeService.InitializeAsync();
|
2026-03-19 10:26:17 +01:00
|
|
|
|
2026-03-20 12:43:09 +01:00
|
|
|
if (hasAnyAccount)
|
|
|
|
|
{
|
|
|
|
|
// Wino account loading and activation.
|
|
|
|
|
await LoadInitialWinoAccountAsync();
|
|
|
|
|
await HandlePostActivationAsync(AppInstance.GetCurrent().GetActivatedEventArgs());
|
|
|
|
|
}
|
2026-03-19 10:26:17 +01:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
LogActivation("Theme service initialized.");
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
// If startup task launch, keep window hidden (system tray only).
|
|
|
|
|
// Otherwise, activate the window normally.
|
|
|
|
|
if (isStartupTaskLaunch)
|
|
|
|
|
{
|
|
|
|
|
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Normal launch - show and activate the window.
|
2026-03-02 00:44:29 +01:00
|
|
|
// The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready.
|
2026-02-27 20:12:43 +01:00
|
|
|
MainWindow?.Activate();
|
2025-11-14 11:37:26 +01:00
|
|
|
LogActivation("Window created and activated.");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-03-01 09:47:05 +01:00
|
|
|
private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
|
|
|
|
|
{
|
2026-03-04 00:12:52 +01:00
|
|
|
// AppNotification callbacks are not guaranteed to run on the UI thread.
|
|
|
|
|
// Marshal toast handling to the window dispatcher before touching window APIs.
|
2026-03-01 09:47:05 +01:00
|
|
|
if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args)) == true)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_ = HandleToastActivationAsync(args);
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-22 15:13:39 +01:00
|
|
|
private void TryRegisterAppNotifications()
|
|
|
|
|
{
|
|
|
|
|
var notificationManager = AppNotificationManager.Default;
|
|
|
|
|
|
|
|
|
|
notificationManager.NotificationInvoked -= AppNotificationInvoked;
|
|
|
|
|
notificationManager.NotificationInvoked += AppNotificationInvoked;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
notificationManager.Register();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogActivation($"App notification registration failed: {ex.GetType().Name} - {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Handles toast notification activation scenarios.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task HandleToastActivationAsync(AppNotificationActivatedEventArgs toastArgs)
|
|
|
|
|
{
|
|
|
|
|
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
2025-10-21 22:08:56 +02:00
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) &&
|
|
|
|
|
storeUpdateAction == Constants.ToastStoreUpdateActionInstall)
|
|
|
|
|
{
|
|
|
|
|
await HandleStoreUpdateToastAsync();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 01:49:29 +01:00
|
|
|
// Check calendar reminder toast activation first.
|
|
|
|
|
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
|
|
|
|
toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) &&
|
|
|
|
|
Guid.TryParse(calendarItemIdString, out Guid calendarItemId))
|
|
|
|
|
{
|
2026-03-04 00:12:52 +01:00
|
|
|
if (calendarAction == Constants.ToastCalendarNavigateAction)
|
|
|
|
|
{
|
|
|
|
|
await HandleCalendarToastNavigationAsync(calendarItemId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (calendarAction == Constants.ToastCalendarSnoozeAction)
|
|
|
|
|
{
|
|
|
|
|
await HandleCalendarToastSnoozeAsync(toastArgs, calendarItemId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-11 01:49:29 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
// Check if this is a navigation toast (user clicked the notification).
|
|
|
|
|
if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation action) &&
|
|
|
|
|
Guid.TryParse(toastArguments[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId))
|
2025-10-21 15:40:19 +02:00
|
|
|
{
|
2025-11-14 11:37:26 +01:00
|
|
|
if (action == MailOperation.Navigate)
|
|
|
|
|
{
|
|
|
|
|
// User clicked notification - create window if needed and navigate.
|
|
|
|
|
await HandleToastNavigationAsync(mailItemUniqueId);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// User clicked action button (Mark as Read, Delete, etc.)
|
|
|
|
|
// Execute action without window and exit.
|
2025-11-14 12:12:13 +01:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
await HandleToastActionAsync(action, mailItemUniqueId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
private async Task HandleStoreUpdateToastAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!IsAppRunning())
|
|
|
|
|
{
|
|
|
|
|
await CreateAndActivateWindow(null!);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
EnsureMainWindowVisibleAndForeground();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var storeUpdateService = Services.GetRequiredService<IStoreUpdateService>();
|
|
|
|
|
await storeUpdateService.StartUpdateAsync();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 01:49:29 +01:00
|
|
|
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
|
|
|
|
|
{
|
|
|
|
|
var calendarService = Services.GetRequiredService<ICalendarService>();
|
|
|
|
|
var navigationService = Services.GetRequiredService<INavigationService>();
|
|
|
|
|
|
2026-03-01 09:47:05 +01:00
|
|
|
var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId);
|
2026-02-11 01:49:29 +01:00
|
|
|
if (calendarItem == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var target = new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single);
|
|
|
|
|
|
|
|
|
|
if (!IsAppRunning())
|
|
|
|
|
{
|
|
|
|
|
await CreateAndActivateWindow(null!);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-03-01 09:47:05 +01:00
|
|
|
EnsureMainWindowVisibleAndForeground();
|
2026-02-11 01:49:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Calendar);
|
|
|
|
|
navigationService.Navigate(WinoPage.EventDetailsPage, target);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 00:12:52 +01:00
|
|
|
private async Task HandleCalendarToastSnoozeAsync(AppNotificationActivatedEventArgs toastArgs, Guid calendarItemId)
|
|
|
|
|
{
|
|
|
|
|
if (!TryGetSnoozeDurationMinutes(toastArgs, out var snoozeDurationMinutes))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var calendarService = Services.GetRequiredService<ICalendarService>();
|
|
|
|
|
var snoozedUntilLocal = DateTime.Now.AddMinutes(snoozeDurationMinutes);
|
|
|
|
|
|
|
|
|
|
await calendarService.SnoozeCalendarItemAsync(calendarItemId, snoozedUntilLocal).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TryGetSnoozeDurationMinutes(AppNotificationActivatedEventArgs toastArgs, out int snoozeDurationMinutes)
|
|
|
|
|
{
|
|
|
|
|
snoozeDurationMinutes = 0;
|
|
|
|
|
|
|
|
|
|
if (toastArgs.UserInput == null ||
|
|
|
|
|
!toastArgs.UserInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) ||
|
|
|
|
|
selectedValue == null)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var selectedText = selectedValue.ToString();
|
|
|
|
|
|
|
|
|
|
return int.TryParse(selectedText, out snoozeDurationMinutes) && snoozeDurationMinutes > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Handles toast notification click for navigation.
|
|
|
|
|
/// Creates window if not running, sets up navigation parameter.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task HandleToastNavigationAsync(Guid mailItemUniqueId)
|
|
|
|
|
{
|
|
|
|
|
var mailService = Services.GetRequiredService<IMailService>();
|
2026-02-22 15:13:39 +01:00
|
|
|
var navigationService = Services.GetRequiredService<INavigationService>();
|
2025-11-14 11:37:26 +01:00
|
|
|
|
2026-03-01 09:47:05 +01:00
|
|
|
var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId);
|
2025-11-14 11:37:26 +01:00
|
|
|
if (account == null) return;
|
|
|
|
|
|
2026-03-01 09:47:05 +01:00
|
|
|
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId);
|
2025-11-14 11:37:26 +01:00
|
|
|
if (mailItem == null) return;
|
|
|
|
|
|
|
|
|
|
var message = new AccountMenuItemExtended(mailItem.AssignedFolder.Id, mailItem);
|
|
|
|
|
|
|
|
|
|
// Store navigation parameter in LaunchProtocolService so AppShell can pick it up.
|
|
|
|
|
var launchProtocolService = Services.GetRequiredService<ILaunchProtocolService>();
|
|
|
|
|
launchProtocolService.LaunchParameter = message;
|
|
|
|
|
|
|
|
|
|
// Create window if not already created.
|
|
|
|
|
if (!IsAppRunning())
|
|
|
|
|
{
|
|
|
|
|
// Pass null for args since we're handling toast navigation
|
|
|
|
|
await CreateAndActivateWindow(null!);
|
2026-02-22 17:55:57 +01:00
|
|
|
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
|
2025-11-14 11:37:26 +01:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// App is already running - send message and bring window to front.
|
2026-02-22 15:13:39 +01:00
|
|
|
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
|
2025-11-14 11:37:26 +01:00
|
|
|
WeakReferenceMessenger.Default.Send(message);
|
2026-03-01 09:47:05 +01:00
|
|
|
EnsureMainWindowVisibleAndForeground();
|
2025-11-14 11:37:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Handles toast action button clicks (Mark as Read, Delete, etc.).
|
|
|
|
|
/// Executes the action without showing UI and exits the app.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task HandleToastActionAsync(MailOperation action, Guid mailItemUniqueId)
|
|
|
|
|
{
|
|
|
|
|
LogActivation($"Handling toast action: {action} for mail {mailItemUniqueId}");
|
|
|
|
|
|
|
|
|
|
var mailService = Services.GetRequiredService<IMailService>();
|
|
|
|
|
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId);
|
|
|
|
|
|
|
|
|
|
if (mailItem == null)
|
|
|
|
|
{
|
|
|
|
|
LogActivation("Mail item not found. Exiting.");
|
2026-03-20 12:43:09 +01:00
|
|
|
ExitApplication();
|
2025-11-14 11:37:26 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var package = new MailOperationPreperationRequest(action, mailItem);
|
|
|
|
|
|
|
|
|
|
// Check if app is already running (has a window).
|
|
|
|
|
if (IsAppRunning())
|
|
|
|
|
{
|
|
|
|
|
// App is running - use the simple delegator pattern.
|
|
|
|
|
// The synchronization will happen in the background.
|
|
|
|
|
LogActivation("App is running. Queueing request via delegator.");
|
|
|
|
|
|
|
|
|
|
var delegator = Services.GetRequiredService<IWinoRequestDelegator>();
|
|
|
|
|
await delegator.ExecuteAsync(package);
|
|
|
|
|
|
|
|
|
|
// Don't exit - app continues running.
|
|
|
|
|
LogActivation($"Toast action {action} queued successfully.");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// App is not running - we need to wait for sync before exiting.
|
|
|
|
|
LogActivation("App is not running. Executing synchronization and waiting for completion.");
|
|
|
|
|
|
|
|
|
|
if (_synchronizationManager == null)
|
|
|
|
|
{
|
|
|
|
|
LogActivation("Synchronization manager is not initialized. Exiting.");
|
2026-03-20 12:43:09 +01:00
|
|
|
ExitApplication();
|
2025-11-14 11:37:26 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var processor = Services.GetRequiredService<IWinoRequestProcessor>();
|
|
|
|
|
var notificationBuilder = Services.GetRequiredService<INotificationBuilder>();
|
|
|
|
|
|
|
|
|
|
// Prepare the requests for the action.
|
|
|
|
|
var requests = await processor.PrepareRequestsAsync(package);
|
|
|
|
|
|
|
|
|
|
if (requests != null && requests.Any())
|
|
|
|
|
{
|
|
|
|
|
// Group requests by account ID (usually just one account).
|
|
|
|
|
var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id);
|
|
|
|
|
|
|
|
|
|
foreach (var accountGroup in accountIds)
|
|
|
|
|
{
|
|
|
|
|
var accountId = accountGroup.Key;
|
|
|
|
|
|
|
|
|
|
// Queue all requests for this account.
|
|
|
|
|
foreach (var request in accountGroup)
|
|
|
|
|
{
|
|
|
|
|
await _synchronizationManager.QueueRequestAsync(request, accountId, triggerSynchronization: false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create synchronization options to execute the queued requests.
|
|
|
|
|
var syncOptions = new MailSynchronizationOptions()
|
|
|
|
|
{
|
|
|
|
|
AccountId = accountId,
|
|
|
|
|
Type = MailSynchronizationType.ExecuteRequests
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
LogActivation($"Executing synchronization for account {accountId}...");
|
|
|
|
|
|
|
|
|
|
// Wait for synchronization to complete before exiting.
|
|
|
|
|
var syncResult = await _synchronizationManager.SynchronizeMailAsync(syncOptions);
|
|
|
|
|
|
|
|
|
|
LogActivation($"Toast action {action} completed. Sync result: {syncResult.CompletedState}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await notificationBuilder.UpdateTaskbarIconBadgeAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogActivation("Toast action handling complete. Exiting app.");
|
|
|
|
|
|
|
|
|
|
// Exit the app after synchronization is complete.
|
2026-03-20 12:43:09 +01:00
|
|
|
ExitApplication();
|
2025-10-21 15:40:19 +02:00
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Creates the main window and activates it.
|
|
|
|
|
/// </summary>
|
2026-02-22 15:13:39 +01:00
|
|
|
private async Task CreateAndActivateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args)
|
2025-11-14 11:37:26 +01:00
|
|
|
{
|
|
|
|
|
CreateWindow(args);
|
|
|
|
|
|
|
|
|
|
// Initialize theme service after window is created.
|
|
|
|
|
await NewThemeService.InitializeAsync();
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
if (MainWindow != null)
|
|
|
|
|
Services.GetRequiredService<IWinoWindowManager>().ActivateWindow(MainWindow);
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
LogActivation("Window created and activated.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
public Task OpenManageAccountsFromWelcomeAsync()
|
|
|
|
|
{
|
|
|
|
|
Services.GetRequiredService<INavigationService>()
|
2026-03-12 14:55:07 +01:00
|
|
|
.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.DrillIn);
|
2026-03-05 10:12:03 +01:00
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Creates the main window without activating it.
|
|
|
|
|
/// Used for both normal launch and startup task launch (tray only).
|
|
|
|
|
/// </summary>
|
2026-02-22 15:13:39 +01:00
|
|
|
private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args)
|
2025-11-14 11:37:26 +01:00
|
|
|
{
|
|
|
|
|
LogActivation("Creating main window.");
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
MainWindow = windowManager.CreateWindow(WinoWindowKind.Shell, () => new ShellWindow());
|
2026-03-01 09:47:05 +01:00
|
|
|
InitializeNavigationDispatcher();
|
2025-11-14 11:37:26 +01:00
|
|
|
|
|
|
|
|
if (MainWindow is not IWinoShellWindow shellWindow)
|
|
|
|
|
throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Shell, shellWindow.GetMainFrame());
|
|
|
|
|
|
2026-02-22 15:13:39 +01:00
|
|
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
|
|
|
|
|
|
|
|
|
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
|
|
|
|
activationArgs.Data is ILaunchActivatedEventArgs launchArgs)
|
|
|
|
|
{
|
2026-02-27 11:00:25 +01:00
|
|
|
var launchArguments = launchArgs.Arguments;
|
|
|
|
|
|
|
|
|
|
if (Program.TryConsumeCurrentProcessAlternateModeOverride())
|
|
|
|
|
{
|
|
|
|
|
launchArguments = AppendLaunchArgument(launchArguments, ToggleDefaultModeLaunchArgument);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shellWindow.HandleAppActivation(launchArguments, launchArgs.TileId, Environment.CommandLine);
|
2026-02-22 15:13:39 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 17:55:57 +01:00
|
|
|
if (TryResolveActivationMode(activationArgs, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var activationMode))
|
2026-02-22 15:13:39 +01:00
|
|
|
{
|
|
|
|
|
shellWindow.HandleAppActivation(GetModeLaunchArgument(activationMode));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shellWindow.HandleAppActivation(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine);
|
2025-11-14 11:37:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
private void CreateWelcomeWindow()
|
|
|
|
|
{
|
|
|
|
|
LogActivation("Creating welcome window.");
|
|
|
|
|
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
MainWindow = windowManager.CreateWindow(WinoWindowKind.Welcome, () => new WelcomeWindow());
|
|
|
|
|
if (MainWindow is WelcomeWindow welcomeWindow)
|
2026-03-09 14:18:13 +01:00
|
|
|
{
|
|
|
|
|
var rootFrame = welcomeWindow.GetRootFrame();
|
|
|
|
|
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Welcome, rootFrame);
|
2026-03-05 10:12:03 +01:00
|
|
|
|
2026-03-09 14:18:13 +01:00
|
|
|
if (rootFrame.Content is WelcomeHostPage welcomeHostPage)
|
|
|
|
|
{
|
|
|
|
|
welcomeHostPage.ResetWizard();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-03-27 14:45:36 +01:00
|
|
|
rootFrame.BackStack.Clear();
|
|
|
|
|
rootFrame.ForwardStack.Clear();
|
|
|
|
|
rootFrame.Navigate(typeof(WelcomeHostPage), null, new SuppressNavigationTransitionInfo());
|
2026-03-09 14:18:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-05 10:12:03 +01:00
|
|
|
|
2026-03-09 14:18:13 +01:00
|
|
|
InitializeNavigationDispatcher();
|
2026-03-05 10:12:03 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-01 09:47:05 +01:00
|
|
|
private void InitializeNavigationDispatcher()
|
|
|
|
|
{
|
|
|
|
|
if (MainWindow == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (Services.GetService<IDispatcher>() is WinUIDispatcher dispatcher)
|
|
|
|
|
{
|
|
|
|
|
dispatcher.Initialize(MainWindow.DispatcherQueue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureMainWindowVisibleAndForeground()
|
|
|
|
|
{
|
2026-03-05 10:12:03 +01:00
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
var currentWindow = windowManager.ActiveWindow
|
|
|
|
|
?? windowManager.GetWindow(WinoWindowKind.Shell)
|
|
|
|
|
?? windowManager.GetWindow(WinoWindowKind.Welcome)
|
|
|
|
|
?? MainWindow;
|
|
|
|
|
|
|
|
|
|
if (currentWindow == null)
|
2026-03-01 09:47:05 +01:00
|
|
|
return;
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
MainWindow = currentWindow;
|
|
|
|
|
windowManager.ActivateWindow(currentWindow);
|
2026-03-01 09:47:05 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-21 01:27:29 +02:00
|
|
|
private void RegisterRecipients()
|
|
|
|
|
{
|
|
|
|
|
WeakReferenceMessenger.Default.Register<NewMailSynchronizationRequested>(this);
|
2025-12-26 20:46:48 +01:00
|
|
|
WeakReferenceMessenger.Default.Register<NewCalendarSynchronizationRequested>(this);
|
2026-03-05 10:12:03 +01:00
|
|
|
WeakReferenceMessenger.Default.Register<AccountCreatedMessage>(this);
|
2026-03-06 03:42:08 +01:00
|
|
|
WeakReferenceMessenger.Default.Register<AccountRemovedMessage>(this);
|
2026-03-05 10:12:03 +01:00
|
|
|
WeakReferenceMessenger.Default.Register<GetStartedFromWelcomeRequested>(this);
|
2025-10-21 01:27:29 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-07 19:47:21 +01:00
|
|
|
public async void Receive(NewMailSynchronizationRequested message)
|
|
|
|
|
{
|
|
|
|
|
if (_synchronizationManager == null) return;
|
|
|
|
|
|
|
|
|
|
MailSynchronizationResult syncResult;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
syncResult = await _synchronizationManager.SynchronizeMailAsync(message.Options);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
// Defensive fallback to guarantee completion message emission.
|
|
|
|
|
syncResult = MailSynchronizationResult.Failed(ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WeakReferenceMessenger.Default.Send(new AccountSynchronizationCompleted(
|
|
|
|
|
message.Options.AccountId,
|
|
|
|
|
syncResult.CompletedState,
|
|
|
|
|
message.Options.GroupedSynchronizationTrackingId));
|
|
|
|
|
|
|
|
|
|
if (syncResult.CompletedState == SynchronizationCompletedState.Failed ||
|
|
|
|
|
syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted)
|
|
|
|
|
{
|
|
|
|
|
var dialogService = Services.GetRequiredService<IMailDialogService>();
|
|
|
|
|
var errorMessage = GetSynchronizationFailureMessage(message.Options.Type, syncResult.Exception?.Message);
|
|
|
|
|
var severity = syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted
|
|
|
|
|
? InfoBarMessageType.Warning
|
|
|
|
|
: InfoBarMessageType.Error;
|
|
|
|
|
|
|
|
|
|
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, errorMessage, severity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async void Receive(NewCalendarSynchronizationRequested message)
|
|
|
|
|
{
|
|
|
|
|
if (_synchronizationManager == null) return;
|
|
|
|
|
|
|
|
|
|
var calendarSyncResult = await _synchronizationManager.SynchronizeCalendarAsync(message.Options);
|
|
|
|
|
|
|
|
|
|
if (calendarSyncResult.CompletedState == SynchronizationCompletedState.Failed)
|
|
|
|
|
{
|
|
|
|
|
var dialogService = Services.GetRequiredService<IMailDialogService>();
|
|
|
|
|
dialogService.InfoBarMessage(
|
|
|
|
|
Translator.Info_SyncFailedTitle,
|
|
|
|
|
Translator.Exception_FailedToSynchronizeFolders,
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
public void Receive(AccountCreatedMessage message)
|
|
|
|
|
{
|
2026-03-20 12:43:09 +01:00
|
|
|
_hasConfiguredAccounts = true;
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
2026-03-14 14:14:58 +01:00
|
|
|
var navigationService = Services.GetRequiredService<INavigationService>();
|
2026-03-05 10:12:03 +01:00
|
|
|
|
|
|
|
|
// Only transition when the account was created from the WelcomeWindow.
|
|
|
|
|
if (windowManager.GetWindow(WinoWindowKind.Welcome) == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
|
|
|
|
{
|
|
|
|
|
// Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher.
|
|
|
|
|
CreateWindow(null);
|
2026-03-20 12:43:09 +01:00
|
|
|
CloseWelcomeWindowIfPresent();
|
2026-03-14 14:14:58 +01:00
|
|
|
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
|
2026-03-20 12:43:09 +01:00
|
|
|
if (MainWindow != null)
|
|
|
|
|
await ActivateWindowAsync(MainWindow);
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
RestartAutoSynchronizationLoop();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 03:42:08 +01:00
|
|
|
public void Receive(AccountRemovedMessage message)
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
|
|
|
|
|
// Only handle when ShellWindow is active (not during wizard rollback)
|
|
|
|
|
if (windowManager.GetWindow(WinoWindowKind.Shell) == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
|
|
|
|
{
|
|
|
|
|
var accounts = await _accountService!.GetAccountsAsync();
|
2026-03-20 12:43:09 +01:00
|
|
|
_hasConfiguredAccounts = accounts.Any();
|
|
|
|
|
if (_hasConfiguredAccounts) return;
|
2026-03-06 03:42:08 +01:00
|
|
|
|
|
|
|
|
// All accounts removed — go back to welcome wizard from step 1
|
|
|
|
|
Services.GetRequiredService<WelcomeWizardContext>().Reset();
|
|
|
|
|
StopAutoSynchronizationLoop();
|
2026-03-27 14:45:36 +01:00
|
|
|
CloseShellWindowIfPresent();
|
2026-03-06 03:42:08 +01:00
|
|
|
CreateWelcomeWindow();
|
2026-03-20 12:43:09 +01:00
|
|
|
if (MainWindow != null)
|
|
|
|
|
await ActivateWindowAsync(MainWindow);
|
2026-03-06 03:42:08 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 10:12:03 +01:00
|
|
|
public void Receive(GetStartedFromWelcomeRequested message)
|
|
|
|
|
{
|
|
|
|
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
|
|
|
|
|
|
|
|
|
if (windowManager.GetWindow(WinoWindowKind.Welcome) == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
|
|
|
|
{
|
|
|
|
|
CreateWindow(null);
|
|
|
|
|
windowManager.HideWindow(WinoWindowKind.Welcome);
|
|
|
|
|
await NewThemeService.ApplyThemeToActiveWindowAsync();
|
|
|
|
|
MainWindow?.Activate();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 19:47:21 +01:00
|
|
|
private static string GetSynchronizationFailureMessage(MailSynchronizationType synchronizationType, string? exceptionMessage)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(exceptionMessage))
|
|
|
|
|
{
|
|
|
|
|
return exceptionMessage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return synchronizationType switch
|
|
|
|
|
{
|
|
|
|
|
MailSynchronizationType.Alias => Translator.Exception_FailedToSynchronizeAliases,
|
|
|
|
|
MailSynchronizationType.UpdateProfile => Translator.Exception_FailedToSynchronizeProfileInformation,
|
|
|
|
|
_ => Translator.Exception_FailedToSynchronizeFolders
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-11-14 12:51:19 +01:00
|
|
|
|
2026-02-11 14:50:59 +01:00
|
|
|
private void PreferencesServiceChanged(object? sender, string propertyName)
|
|
|
|
|
{
|
|
|
|
|
if (propertyName != nameof(IPreferencesService.EmailSyncIntervalMinutes))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
RestartAutoSynchronizationLoop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RestartAutoSynchronizationLoop()
|
|
|
|
|
{
|
|
|
|
|
if (_preferencesService == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
StopAutoSynchronizationLoop();
|
|
|
|
|
|
|
|
|
|
int intervalMinutes = Math.Max(1, _preferencesService.EmailSyncIntervalMinutes);
|
|
|
|
|
_autoSynchronizationLoopCts = new CancellationTokenSource();
|
|
|
|
|
|
|
|
|
|
_ = RunAutoSynchronizationLoopAsync(TimeSpan.FromMinutes(intervalMinutes), _autoSynchronizationLoopCts.Token);
|
|
|
|
|
LogActivation($"Automatic sync loop started. Interval: {intervalMinutes} minute(s).");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StopAutoSynchronizationLoop()
|
|
|
|
|
{
|
|
|
|
|
if (_autoSynchronizationLoopCts == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_autoSynchronizationLoopCts.Cancel();
|
|
|
|
|
_autoSynchronizationLoopCts.Dispose();
|
|
|
|
|
_autoSynchronizationLoopCts = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 01:33:27 +01:00
|
|
|
private async Task LoadInitialWinoAccountAsync()
|
|
|
|
|
{
|
|
|
|
|
var winoAccountProfileService = Services.GetRequiredService<IWinoAccountProfileService>();
|
|
|
|
|
var winoAccount = await winoAccountProfileService.GetActiveAccountAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (winoAccount != null)
|
|
|
|
|
{
|
2026-03-19 10:26:17 +01:00
|
|
|
WeakReferenceMessenger.Default.Send(new WinoAccountProfileUpdatedMessage(winoAccount));
|
2026-03-16 01:33:27 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 14:50:59 +01:00
|
|
|
private async Task RunAutoSynchronizationLoopAsync(TimeSpan interval, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await ExecuteAutoSynchronizationAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
using var timer = new PeriodicTimer(interval);
|
|
|
|
|
|
|
|
|
|
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
|
|
|
|
|
{
|
|
|
|
|
await ExecuteAutoSynchronizationAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
// no-op
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogActivation($"Automatic sync loop failed: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ExecuteAutoSynchronizationAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (_synchronizationManager == null || _accountService == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
bool lockTaken = false;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
lockTaken = await _autoSynchronizationSemaphore.WaitAsync(0, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (!lockTaken)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
|
|
|
|
var currentAccountIds = accounts.Select(a => a.Id).ToHashSet();
|
|
|
|
|
_inboxSyncCounters.Keys.Where(a => !currentAccountIds.Contains(a)).ToList().ForEach(a => _inboxSyncCounters.Remove(a));
|
|
|
|
|
|
|
|
|
|
foreach (var account in accounts)
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
if (_synchronizationManager.IsAccountSynchronizing(account.Id))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var inboxSyncOptions = new MailSynchronizationOptions()
|
|
|
|
|
{
|
|
|
|
|
AccountId = account.Id,
|
|
|
|
|
Type = MailSynchronizationType.InboxOnly
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var inboxSyncResult = await _synchronizationManager.SynchronizeMailAsync(inboxSyncOptions, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (inboxSyncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted)
|
|
|
|
|
{
|
|
|
|
|
_inboxSyncCounters.TryAdd(account.Id, 0);
|
|
|
|
|
_inboxSyncCounters[account.Id]++;
|
|
|
|
|
|
|
|
|
|
if (_inboxSyncCounters[account.Id] >= InboxSyncsPerFullSync)
|
|
|
|
|
{
|
|
|
|
|
var fullSyncOptions = new MailSynchronizationOptions()
|
|
|
|
|
{
|
|
|
|
|
AccountId = account.Id,
|
|
|
|
|
Type = MailSynchronizationType.FullFolders
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await _synchronizationManager.SynchronizeMailAsync(fullSyncOptions, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
_inboxSyncCounters[account.Id] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!account.IsCalendarAccessGranted)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var calendarOptions = new CalendarSynchronizationOptions()
|
|
|
|
|
{
|
|
|
|
|
AccountId = account.Id,
|
|
|
|
|
Type = CalendarSynchronizationType.CalendarMetadata
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await _synchronizationManager.SynchronizeCalendarAsync(calendarOptions, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (lockTaken)
|
|
|
|
|
{
|
|
|
|
|
_autoSynchronizationSemaphore.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 12:51:19 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Handles activation redirected from another instance (single-instancing).
|
|
|
|
|
/// This is called when a second instance tries to launch and redirects to this existing instance.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void HandleRedirectedActivation(AppActivationArguments args)
|
|
|
|
|
{
|
|
|
|
|
// Dispatch to UI thread since this is called from Program.OnActivated
|
2026-03-19 10:26:17 +01:00
|
|
|
MainWindow?.DispatcherQueue.TryEnqueue(async () =>
|
2025-11-14 12:51:19 +01:00
|
|
|
{
|
|
|
|
|
// Handle different activation kinds
|
|
|
|
|
if (args.Kind == ExtendedActivationKind.AppNotification)
|
|
|
|
|
{
|
|
|
|
|
// Handle toast notification activation
|
|
|
|
|
var toastArgs = (AppNotificationActivatedEventArgs)args.Data;
|
|
|
|
|
_ = HandleToastActivationAsync(toastArgs);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-02-22 15:13:39 +01:00
|
|
|
if (MainWindow is IWinoShellWindow shellWindow)
|
|
|
|
|
{
|
|
|
|
|
if (args.Kind == ExtendedActivationKind.Launch &&
|
|
|
|
|
args.Data is ILaunchActivatedEventArgs launchArgs)
|
|
|
|
|
{
|
2026-02-27 11:00:25 +01:00
|
|
|
var launchArguments = launchArgs.Arguments;
|
|
|
|
|
|
|
|
|
|
if (Program.TryConsumeRedirectedAlternateModeOverride())
|
|
|
|
|
{
|
|
|
|
|
launchArguments = AppendLaunchArgument(launchArguments, ToggleDefaultModeLaunchArgument);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shellWindow.HandleAppActivation(launchArguments, launchArgs.TileId);
|
2026-02-22 15:13:39 +01:00
|
|
|
}
|
2026-02-22 17:55:57 +01:00
|
|
|
else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode))
|
2026-02-22 15:13:39 +01:00
|
|
|
{
|
|
|
|
|
shellWindow.HandleAppActivation(GetModeLaunchArgument(redirectedMode));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 10:26:17 +01:00
|
|
|
await HandlePostActivationAsync(args);
|
|
|
|
|
|
2026-02-22 15:13:39 +01:00
|
|
|
// Bring the existing window to front after handling redirected activation.
|
2025-11-14 12:51:19 +01:00
|
|
|
MainWindow?.BringToFront();
|
|
|
|
|
MainWindow?.Activate();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-26 20:46:48 +01:00
|
|
|
|
2026-02-22 15:13:39 +01:00
|
|
|
private static string GetModeLaunchArgument(WinoApplicationMode mode)
|
2026-03-10 16:50:16 +01:00
|
|
|
=> mode switch
|
|
|
|
|
{
|
|
|
|
|
WinoApplicationMode.Calendar => "--mode=calendar",
|
|
|
|
|
WinoApplicationMode.Contacts => "--mode=contacts",
|
2026-03-12 19:04:47 +01:00
|
|
|
WinoApplicationMode.Settings => "--mode=settings",
|
2026-03-10 16:50:16 +01:00
|
|
|
_ => "--mode=mail"
|
|
|
|
|
};
|
2026-02-22 15:13:39 +01:00
|
|
|
|
2026-02-27 11:00:25 +01:00
|
|
|
private static string AppendLaunchArgument(string? launchArguments, string launchArgument)
|
|
|
|
|
{
|
|
|
|
|
return string.IsNullOrWhiteSpace(launchArguments)
|
|
|
|
|
? launchArgument
|
|
|
|
|
: $"{launchArguments} {launchArgument}";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 17:55:57 +01:00
|
|
|
private static bool TryResolveActivationMode(AppActivationArguments activationArgs, WinoApplicationMode defaultMode, out WinoApplicationMode mode)
|
2026-02-22 15:13:39 +01:00
|
|
|
{
|
2026-02-22 17:55:57 +01:00
|
|
|
mode = defaultMode;
|
2026-02-22 15:13:39 +01:00
|
|
|
|
|
|
|
|
if (activationArgs.Kind == ExtendedActivationKind.Protocol &&
|
|
|
|
|
activationArgs.Data is IProtocolActivatedEventArgs protocolArgs)
|
|
|
|
|
{
|
|
|
|
|
var scheme = protocolArgs.Uri?.Scheme;
|
|
|
|
|
|
|
|
|
|
if (string.Equals(scheme, "webcal", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(scheme, "webcals", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
mode = WinoApplicationMode.Calendar;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (string.Equals(scheme, "mailto", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(scheme, "google.pw.oauth2", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
mode = WinoApplicationMode.Mail;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-03-19 10:26:17 +01:00
|
|
|
|
|
|
|
|
if (string.Equals(scheme, WinoProtocolScheme, StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
string.Equals(protocolArgs.Uri?.Host, BillingProtocolHost, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
mode = WinoApplicationMode.Settings;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-02-22 15:13:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activationArgs.Kind == ExtendedActivationKind.File &&
|
|
|
|
|
activationArgs.Data is IFileActivatedEventArgs fileArgs)
|
|
|
|
|
{
|
|
|
|
|
var fileItem = fileArgs.Files?.FirstOrDefault();
|
|
|
|
|
var extension = Path.GetExtension(fileItem?.Name ?? string.Empty);
|
|
|
|
|
|
|
|
|
|
if (string.Equals(extension, ".ics", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
mode = WinoApplicationMode.Calendar;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (string.Equals(extension, ".eml", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
mode = WinoApplicationMode.Mail;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
|
|
|
|
activationArgs.Data is ILaunchActivatedEventArgs launchArgs)
|
|
|
|
|
{
|
2026-02-22 17:55:57 +01:00
|
|
|
mode = AppModeActivationResolver.Resolve(launchArgs.Arguments, launchArgs.TileId, null, defaultMode);
|
2026-02-22 15:13:39 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string? GetCurrentLaunchTileId()
|
|
|
|
|
{
|
|
|
|
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
|
|
|
|
|
|
|
|
|
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
|
|
|
|
activationArgs.Data is ILaunchActivatedEventArgs launchArgs)
|
|
|
|
|
{
|
|
|
|
|
return launchArgs.TileId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-03-19 10:26:17 +01:00
|
|
|
|
|
|
|
|
private async Task HandlePostActivationAsync(AppActivationArguments activationArgs)
|
|
|
|
|
{
|
|
|
|
|
if (await TryHandleBillingProtocolActivationAsync(activationArgs).ConfigureAwait(false))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<bool> TryHandleBillingProtocolActivationAsync(AppActivationArguments activationArgs)
|
|
|
|
|
{
|
|
|
|
|
if (!TryGetBillingCallbackUri(activationArgs, out var callbackUri))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Services.GetRequiredService<INavigationService>().Navigate(
|
|
|
|
|
WinoPage.SettingsPage,
|
|
|
|
|
WinoPage.WinoAccountManagementPage,
|
|
|
|
|
NavigationReferenceFrame.ShellFrame,
|
|
|
|
|
NavigationTransitionType.None);
|
|
|
|
|
|
|
|
|
|
var winoAccountProfileService = Services.GetRequiredService<IWinoAccountProfileService>();
|
|
|
|
|
await winoAccountProfileService.ProcessBillingCallbackAsync(callbackUri).ConfigureAwait(false);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TryGetBillingCallbackUri(AppActivationArguments activationArgs, out Uri callbackUri)
|
|
|
|
|
{
|
|
|
|
|
callbackUri = null!;
|
|
|
|
|
|
|
|
|
|
if (activationArgs.Kind != ExtendedActivationKind.Protocol ||
|
|
|
|
|
activationArgs.Data is not IProtocolActivatedEventArgs protocolArgs ||
|
|
|
|
|
protocolArgs.Uri == null)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var uri = protocolArgs.Uri;
|
|
|
|
|
if (!string.Equals(uri.Scheme, WinoProtocolScheme, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
!string.Equals(uri.Host, BillingProtocolHost, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
!string.Equals(uri.AbsolutePath, BillingSuccessPath, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
callbackUri = uri;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
2026-03-08 11:22:41 +01:00
|
|
|
|
|
|
|
|
|