New startup window.

This commit is contained in:
Burak Kaan Köse
2026-03-05 10:12:03 +01:00
parent d45d3faa89
commit db5ecd60e4
46 changed files with 1857 additions and 234 deletions
+52 -13
View File
@@ -13,6 +13,7 @@ using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
using Wino.Mail.WinUI.Services;
using Wino.Mail.WinUI.Views.Calendar;
using Wino.Messaging.Client.Mails;
@@ -29,6 +30,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
{
private readonly IStatePersistanceService _statePersistanceService;
private readonly IDispatcher _dispatcher;
private readonly IWinoWindowManager _windowManager;
private WinoPage[] _renderingPageTypes = new WinoPage[]
{
@@ -42,7 +44,8 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.MailRenderingPage,
WinoPage.ComposePage,
WinoPage.IdlePage,
WinoPage.WelcomePage
WinoPage.WelcomePage,
WinoPage.WelcomePageV2
];
private static readonly WinoPage[] CalendarOnlyPages =
@@ -51,10 +54,11 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.EventDetailsPage
];
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher)
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher, IWinoWindowManager windowManager)
{
_statePersistanceService = statePersistanceService;
_dispatcher = dispatcher;
_windowManager = windowManager;
}
private bool IsOnNavigationThread()
@@ -101,6 +105,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.MailListPage => typeof(MailListPage),
WinoPage.SettingsPage => typeof(SettingsPage),
WinoPage.WelcomePage => typeof(WelcomePage),
WinoPage.WelcomePageV2 => typeof(WelcomePageV2),
WinoPage.SettingOptionsPage => typeof(SettingOptionsPage),
WinoPage.AppPreferencesPage => typeof(AppPreferencesPage),
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
@@ -120,19 +125,45 @@ public class NavigationService : NavigationServiceBase, INavigationService
}
public Frame GetCoreFrame(NavigationReferenceFrame frameType)
=> ExecuteOnNavigationThread(() => GetCoreFrameInternal(frameType));
=> ExecuteOnNavigationThread(() => GetCoreFrameInternal(frameType) ?? throw new ArgumentException($"Frame '{frameType}' cannot be resolved."));
private Frame GetCoreFrameInternal(NavigationReferenceFrame frameType)
private Frame? GetCoreFrameInternal(NavigationReferenceFrame frameType, WinoWindowKind? requestedWindowKind = null)
{
if (WinoApplication.MainWindow is not IWinoShellWindow shellWindow) throw new ArgumentException("MainWindow must implement IWinoShellWindow");
if (shellWindow.GetMainFrame() is not Frame mainFrame) throw new ArgumentException("MainFrame cannot be null.");
if (frameType == NavigationReferenceFrame.ShellFrame)
{
if (requestedWindowKind.HasValue)
return _windowManager.GetPrimaryNavigationFrame(requestedWindowKind.Value);
if (frameType == NavigationReferenceFrame.ShellFrame) return shellWindow.GetMainFrame();
var activeWindow = _windowManager.ActiveWindow;
if (activeWindow != null)
{
var activeShellWindow = _windowManager.GetWindow(WinoWindowKind.Shell);
if (ReferenceEquals(activeWindow, activeShellWindow))
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell);
var contentRoot = mainFrame.Content as UIElement;
if (contentRoot == null) return mainFrame;
var activeWelcomeWindow = _windowManager.GetWindow(WinoWindowKind.Welcome);
if (ReferenceEquals(activeWindow, activeWelcomeWindow))
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Welcome);
}
return WinoVisualTreeHelper.GetChildObject<Frame>(contentRoot, frameType.ToString()) ?? mainFrame;
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell)
?? _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Welcome);
}
var mainFrame = _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell);
if (mainFrame == null)
return null;
var contentRoot = mainFrame.Content as FrameworkElement;
if (contentRoot == null) return null;
// Use FindName first — it works immediately after InitializeComponent(),
// before the visual tree is built by the layout pass.
if (contentRoot.FindName(frameType.ToString()) is Frame namedFrame)
return namedFrame;
// Fall back to visual tree search for deeply nested frames (e.g. RenderingFrame).
return WinoVisualTreeHelper.GetChildObject<Frame>(contentRoot, frameType.ToString());
}
public bool ChangeApplicationMode(WinoApplicationMode mode)
@@ -211,14 +242,22 @@ public class NavigationService : NavigationServiceBase, INavigationService
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage;
Frame innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
Frame? innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
if (innerShellFrame == null && frame == NavigationReferenceFrame.ShellFrame)
{
var requestedFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame, WinoWindowKind.Welcome);
if (requestedFrame == null)
return false;
return requestedFrame.Navigate(pageType, parameter, GetNavigationTransitionInfo(transition));
}
if (innerShellFrame != null)
{
// Calendar navigations.
if (currentApplicationMode == WinoApplicationMode.Calendar)
{
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
var currentFrameType = GetCurrentFrameType(innerShellFrame);
if (page == WinoPage.CalendarPage &&
parameter is CalendarPageNavigationArgs calendarNavigationArgs)
@@ -247,7 +286,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
else
{
// Mail navigations.
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
var currentFrameType = GetCurrentFrameType(innerShellFrame);
bool isMailListingPageActive = currentFrameType != null && currentFrameType == typeof(MailListPage);
// Active page is mail list page and we are refreshing the folder.
@@ -17,10 +17,10 @@ public class NavigationServiceBase
};
}
public Type? GetCurrentFrameType(ref Frame _frame)
public Type? GetCurrentFrameType(Frame frame)
{
if (_frame != null && _frame.Content != null)
return _frame.Content.GetType();
if (frame != null && frame.Content != null)
return frame.Content.GetType();
return null;
}
+60 -15
View File
@@ -62,6 +62,7 @@ public class NewThemeService : INewThemeService
private readonly IConfigurationService _configurationService;
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly IApplicationResourceManager<ResourceDictionary> _applicationResourceManager;
private readonly IWinoWindowManager _windowManager;
private List<AppThemeBase> preDefinedThemes { get; set; } = new List<AppThemeBase>()
{
@@ -75,11 +76,13 @@ public class NewThemeService : INewThemeService
public NewThemeService(IConfigurationService configurationService,
IUnderlyingThemeService underlyingThemeService,
IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
IWinoWindowManager windowManager)
{
_configurationService = configurationService;
_underlyingThemeService = underlyingThemeService;
_applicationResourceManager = applicationResourceManager;
_windowManager = windowManager;
}
/// <summary>
@@ -89,11 +92,17 @@ public class NewThemeService : INewThemeService
{
get
{
return GetShellRootContent().RequestedTheme.ToWinoElementTheme();
var rootContent = TryGetShellRootContent();
if (rootContent == null)
return _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
return rootContent.RequestedTheme.ToWinoElementTheme();
}
set
{
GetShellRootContent().RequestedTheme = value.ToWindowsElementTheme();
var rootContent = TryGetShellRootContent();
if (rootContent != null)
rootContent.RequestedTheme = value.ToWindowsElementTheme();
_configurationService.Set(UnderlyingThemeService.SelectedAppThemeKey, value);
@@ -115,9 +124,10 @@ public class NewThemeService : INewThemeService
_configurationService.Set(CurrentApplicationThemeKey, value);
if (WinoApplication.MainWindow != null)
var window = GetThemeWindow();
if (window != null)
{
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(async () =>
window.DispatcherQueue.TryEnqueue(async () =>
{
await ApplyCustomThemeAsync(false);
});
@@ -154,9 +164,10 @@ public class NewThemeService : INewThemeService
currentBackdropType = value;
_configurationService.Set(WindowBackdropTypeKey, (int)value);
if (WinoApplication.MainWindow != null)
var window = GetThemeWindow();
if (window != null)
{
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(() =>
window.DispatcherQueue.TryEnqueue(() =>
{
ApplyBackdrop(value);
});
@@ -176,7 +187,17 @@ public class NewThemeService : INewThemeService
}
}
public FrameworkElement GetShellRootContent() => (WinoApplication.MainWindow as IWinoShellWindow)?.GetRootContent() ?? throw new Exception("No root content found");
public FrameworkElement GetShellRootContent()
{
var window = GetThemeWindow();
if (window is IWinoShellWindow shellWindow)
return shellWindow.GetRootContent();
if (window?.Content is FrameworkElement frameworkElement)
return frameworkElement;
throw new Exception("No root content found");
}
private bool isInitialized = false;
@@ -210,9 +231,9 @@ public class NewThemeService : INewThemeService
public void ApplyBackdrop(WindowBackdropType backdropType)
{
if (WinoApplication.MainWindow is not WindowEx windowEx)
if (GetThemeWindow() is not WindowEx windowEx)
{
Debug.WriteLine("MainWindow is not WindowEx, cannot apply backdrop");
Debug.WriteLine("No active WindowEx found, cannot apply backdrop");
return;
}
@@ -267,7 +288,7 @@ public class NewThemeService : INewThemeService
private void NotifyThemeUpdate()
{
if (GetShellRootContent() is not UIElement rootContent) return;
if (TryGetShellRootContent() is not UIElement rootContent) return;
_ = rootContent.DispatcherQueue.EnqueueAsync(() =>
{
@@ -283,9 +304,12 @@ public class NewThemeService : INewThemeService
public void UpdateSystemCaptionButtonColors()
{
GetShellRootContent().DispatcherQueue.TryEnqueue(() =>
var rootContent = TryGetShellRootContent();
if (rootContent == null) return;
rootContent.DispatcherQueue.TryEnqueue(() =>
{
if (WinoApplication.MainWindow is not WindowEx mainWindow) return;
if (GetThemeWindow() is not WindowEx mainWindow) return;
var titleBar = mainWindow.AppWindow.TitleBar;
if (titleBar == null) return;
@@ -353,8 +377,7 @@ public class NewThemeService : INewThemeService
private void RefreshThemeResource()
{
var mainApplicationFrame = GetShellRootContent();
var mainApplicationFrame = TryGetShellRootContent();
if (mainApplicationFrame == null) return;
if (mainApplicationFrame.RequestedTheme == ElementTheme.Dark)
@@ -648,4 +671,26 @@ public class NewThemeService : INewThemeService
new BackdropTypeWrapper(WindowBackdropType.AcrylicThin, "Acrylic Thin")
};
}
private WindowEx? GetThemeWindow() => _windowManager.ActiveWindow ?? WinoApplication.MainWindow;
private FrameworkElement? TryGetShellRootContent()
{
var window = GetThemeWindow();
if (window == null)
return null;
if (window is IWinoShellWindow shellWindow)
return shellWindow.GetRootContent();
return window.Content as FrameworkElement;
}
public async Task ApplyThemeToActiveWindowAsync()
{
ApplyBackdrop(currentBackdropType);
RootTheme = _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
await ApplyCustomThemeAsync(false);
UpdateSystemCaptionButtonColors();
}
}
@@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
namespace Wino.Mail.WinUI.Services;
public class WinoWindowManager : IWinoWindowManager
{
public event EventHandler<WindowEx?>? ActiveWindowChanged;
public event EventHandler<WindowEx>? WindowRemoved;
private readonly object _syncLock = new();
private readonly Dictionary<(WinoWindowKind Kind, string Name), WindowEx> _windows = [];
private readonly Dictionary<WindowEx, (WinoWindowKind Kind, string Name)> _windowKeys = [];
private readonly Dictionary<(WinoWindowKind Kind, string Name), Frame> _primaryNavigationFrames = [];
public WindowEx? ActiveWindow { get; private set; }
public WindowEx CreateWindow(WinoWindowKind kind, Func<WindowEx> factory, string? name = null)
{
var key = CreateKey(kind, name);
lock (_syncLock)
{
if (_windows.TryGetValue(key, out var existingWindow))
{
ActiveWindow = existingWindow;
ActiveWindowChanged?.Invoke(this, existingWindow);
return existingWindow;
}
}
var newWindow = factory();
lock (_syncLock)
{
if (_windows.TryGetValue(key, out var existingWindow))
{
ActiveWindow = existingWindow;
ActiveWindowChanged?.Invoke(this, existingWindow);
return existingWindow;
}
TrackWindow(key, newWindow);
ActiveWindow = newWindow;
ActiveWindowChanged?.Invoke(this, newWindow);
return newWindow;
}
}
public WindowEx? GetWindow(WinoWindowKind kind, string? name = null)
{
lock (_syncLock)
{
_windows.TryGetValue(CreateKey(kind, name), out var window);
return window;
}
}
public WindowEx? GetWindow(string name)
{
var normalizedName = NormalizeName(name);
lock (_syncLock)
{
return _windows
.Where(x => x.Key.Name.Equals(normalizedName, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Value)
.FirstOrDefault();
}
}
public void ActivateWindow(WindowEx window)
{
window.Show();
window.BringToFront();
window.Activate();
lock (_syncLock)
{
ActiveWindow = window;
}
ActiveWindowChanged?.Invoke(this, window);
}
public bool ActivateWindow(WinoWindowKind kind, string? name = null)
{
var window = GetWindow(kind, name);
if (window == null)
return false;
ActivateWindow(window);
return true;
}
public void HideWindow(WindowEx window)
{
window.Hide();
lock (_syncLock)
{
if (ReferenceEquals(ActiveWindow, window))
{
ActiveWindow = null;
ActiveWindowChanged?.Invoke(this, null);
}
}
}
public bool HideWindow(WinoWindowKind kind, string? name = null)
{
var window = GetWindow(kind, name);
if (window == null)
return false;
HideWindow(window);
return true;
}
public void SetPrimaryNavigationFrame(WinoWindowKind kind, Frame frame, string? name = null)
{
lock (_syncLock)
{
_primaryNavigationFrames[CreateKey(kind, name)] = frame;
}
}
public Frame? GetPrimaryNavigationFrame(WinoWindowKind kind, string? name = null)
{
lock (_syncLock)
{
_primaryNavigationFrames.TryGetValue(CreateKey(kind, name), out var frame);
return frame;
}
}
private void TrackWindow((WinoWindowKind Kind, string Name) key, WindowEx window)
{
_windows[key] = window;
_windowKeys[window] = key;
window.Activated += WindowActivated;
window.Closed += WindowClosed;
}
private void WindowActivated(object sender, WindowActivatedEventArgs args)
{
if (sender is not WindowEx window)
return;
if (args.WindowActivationState == WindowActivationState.Deactivated)
return;
lock (_syncLock)
{
if (_windowKeys.ContainsKey(window))
{
ActiveWindow = window;
ActiveWindowChanged?.Invoke(this, window);
}
}
}
private void WindowClosed(object sender, WindowEventArgs args)
{
if (sender is not WindowEx window)
return;
lock (_syncLock)
{
if (!_windowKeys.TryGetValue(window, out var key))
return;
window.Activated -= WindowActivated;
window.Closed -= WindowClosed;
_windowKeys.Remove(window);
_windows.Remove(key);
_primaryNavigationFrames.Remove(key);
WindowRemoved?.Invoke(this, window);
if (ReferenceEquals(ActiveWindow, window))
{
ActiveWindow = null;
ActiveWindowChanged?.Invoke(this, null);
}
}
}
public void CloseAllWindows()
{
List<WindowEx> windows;
lock (_syncLock)
{
windows = _windows.Values.Distinct().ToList();
}
foreach (var window in windows)
{
try
{
window.Activated -= WindowActivated;
window.Closed -= WindowClosed;
window.Close();
}
catch
{
// Best effort shutdown for all tracked windows.
}
}
lock (_syncLock)
{
_windowKeys.Clear();
_windows.Clear();
_primaryNavigationFrames.Clear();
ActiveWindow = null;
}
ActiveWindowChanged?.Invoke(this, null);
}
private static (WinoWindowKind Kind, string Name) CreateKey(WinoWindowKind kind, string? name)
{
var resolvedName = NormalizeName(name ?? kind.ToString());
return (kind, string.IsNullOrWhiteSpace(resolvedName) ? kind.ToString() : resolvedName);
}
private static string NormalizeName(string name) => name.Trim();
}