Back navigation and shell improvements.

This commit is contained in:
Burak Kaan Köse
2026-03-24 18:05:09 +01:00
parent d699818c6f
commit 27c90d2f89
10 changed files with 96 additions and 86 deletions
@@ -28,11 +28,6 @@ public interface IStatePersistanceService : INotifyPropertyChanged
/// </summary> /// </summary>
bool IsReaderNarrowed { get; set; } bool IsReaderNarrowed { get; set; }
/// <summary>
/// Should display back button on the shell title bar.
/// </summary>
bool IsBackButtonVisible { get; }
/// <summary> /// <summary>
/// Current application mode (Mail or Calendar). /// Current application mode (Mail or Calendar).
/// Not persisted to configuration, only kept in memory. /// Not persisted to configuration, only kept in memory.
@@ -44,11 +39,6 @@ public interface IStatePersistanceService : INotifyPropertyChanged
/// </summary> /// </summary>
bool IsEventDetailsVisible { get; set; } bool IsEventDetailsVisible { get; set; }
/// <summary>
/// Whether the current application mode has an active backstack that can be navigated.
/// </summary>
bool HasCurrentModeBackStack { get; set; }
/// <summary> /// <summary>
/// Setting: Opened pane length for the navigation view. /// Setting: Opened pane length for the navigation view.
/// </summary> /// </summary>
@@ -13,5 +13,6 @@ public interface INavigationService
Type GetPageType(WinoPage winoPage); Type GetPageType(WinoPage winoPage);
bool ChangeApplicationMode(WinoApplicationMode mode); bool ChangeApplicationMode(WinoApplicationMode mode);
bool CanGoBack();
void GoBack(NavigationTransitionEffect slideEffect = NavigationTransitionEffect.FromRight); void GoBack(NavigationTransitionEffect slideEffect = NavigationTransitionEffect.FromRight);
} }
@@ -20,12 +20,20 @@ public sealed class CalendarRangeTextFormatter : ICalendarRangeTextFormatter
return FormatDate(range.StartDate, culture); return FormatDate(range.StartDate, culture);
} }
if (range.SpansSingleMonth)
{
return $"{FormatDate(range.StartDate, culture)} - {FormatDay(range.EndDate, culture)}";
}
return $"{FormatDate(range.StartDate, culture)} - {FormatDate(range.EndDate, culture)}"; return $"{FormatDate(range.StartDate, culture)} - {FormatDate(range.EndDate, culture)}";
} }
private static string FormatDate(DateOnly date, CultureInfo culture) private static string FormatDate(DateOnly date, CultureInfo culture)
=> date.ToString(culture.DateTimeFormat.MonthDayPattern, culture); => date.ToString(culture.DateTimeFormat.MonthDayPattern, culture);
private static string FormatDay(DateOnly date, CultureInfo culture)
=> date.Day.ToString(culture);
private static string FormatMonth(DateOnly date, CultureInfo culture) private static string FormatMonth(DateOnly date, CultureInfo culture)
=> date.ToString(culture.DateTimeFormat.YearMonthPattern, culture); => date.ToString(culture.DateTimeFormat.YearMonthPattern, culture);
} }
@@ -32,7 +32,7 @@ public class CalendarRangeTextFormatterTests
startDate: new DateOnly(2026, 3, 3), startDate: new DateOnly(2026, 3, 3),
endDate: new DateOnly(2026, 3, 10)); endDate: new DateOnly(2026, 3, 10));
Formatter.Format(range, DateContextProvider).Should().Be("March 3 - March 10"); Formatter.Format(range, DateContextProvider).Should().Be("March 3 - 10");
} }
[Fact] [Fact]
+34 -25
View File
@@ -219,6 +219,9 @@ public class NavigationService : NavigationServiceBase, INavigationService
public bool ChangeApplicationMode(WinoApplicationMode mode) public bool ChangeApplicationMode(WinoApplicationMode mode)
=> ExecuteOnNavigationThread(() => ChangeApplicationModeInternal(mode)); => ExecuteOnNavigationThread(() => ChangeApplicationModeInternal(mode));
public bool CanGoBack()
=> ExecuteOnNavigationThread(CanGoBackInternal);
private bool ChangeApplicationModeInternal(WinoApplicationMode mode, object? activationParameter = null) private bool ChangeApplicationModeInternal(WinoApplicationMode mode, object? activationParameter = null)
{ {
var coreFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame); var coreFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame);
@@ -333,7 +336,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (innerShellFrame.CanGoBack && lastBackStackEntry?.SourcePageType == pageType) if (innerShellFrame.CanGoBack && lastBackStackEntry?.SourcePageType == pageType)
{ {
innerShellFrame.GoBack(); innerShellFrame.GoBack();
UpdateCurrentModeBackStackState(innerShellFrame);
WeakReferenceMessenger.Default.Send(loadCalendarMessage); WeakReferenceMessenger.Default.Send(loadCalendarMessage);
return true; return true;
} }
@@ -495,7 +497,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (navigationResult) if (navigationResult)
{ {
UpdateCurrentModeBackStackState(frame); return true;
} }
return navigationResult; return navigationResult;
@@ -522,15 +524,13 @@ public class NavigationService : NavigationServiceBase, INavigationService
var currentApplicationMode = _statePersistanceService.ApplicationMode; var currentApplicationMode = _statePersistanceService.ApplicationMode;
if (currentApplicationMode == WinoApplicationMode.Settings && if (currentApplicationMode == WinoApplicationMode.Settings &&
_statePersistanceService.HasCurrentModeBackStack) innerShellFrame?.Content is SettingsPage settingsPage)
{
if (settingsPage.CanNavigateBack)
{ {
WeakReferenceMessenger.Default.Send(new BackBreadcrumNavigationRequested(slideEffect)); WeakReferenceMessenger.Default.Send(new BackBreadcrumNavigationRequested(slideEffect));
return;
} }
if (currentApplicationMode == WinoApplicationMode.Settings &&
innerShellFrame?.Content is SettingsPage)
{
return; return;
} }
@@ -544,7 +544,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (innerShellFrame?.CanGoBack == true) if (innerShellFrame?.CanGoBack == true)
{ {
innerShellFrame.GoBack(); innerShellFrame.GoBack();
UpdateCurrentModeBackStackState(innerShellFrame);
} }
else if (innerShellFrame != null && innerShellFrame.Content?.GetType() != typeof(CalendarPage)) else if (innerShellFrame != null && innerShellFrame.Content?.GetType() != typeof(CalendarPage))
{ {
@@ -560,16 +559,11 @@ public class NavigationService : NavigationServiceBase, INavigationService
{ {
// Mail mode: Clear selections and dispose rendering frame // Mail mode: Clear selections and dispose rendering frame
_statePersistanceService.IsReadingMail = false; _statePersistanceService.IsReadingMail = false;
_statePersistanceService.HasCurrentModeBackStack = false;
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested()); WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested()); WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
} }
} }
else
{
UpdateCurrentModeBackStackState(innerShellFrame);
}
} }
private void ResetCurrentModeBackStackState() private void ResetCurrentModeBackStackState()
@@ -581,17 +575,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
innerShellFrame.BackStack.Clear(); innerShellFrame.BackStack.Clear();
innerShellFrame.ForwardStack.Clear(); innerShellFrame.ForwardStack.Clear();
} }
_statePersistanceService.HasCurrentModeBackStack = false;
}
private void UpdateCurrentModeBackStackState(Frame? innerShellFrame)
{
if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Settings)
return;
_statePersistanceService.HasCurrentModeBackStack = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar &&
innerShellFrame?.CanGoBack == true;
} }
private void PruneInnerShellBackStackForMode(Frame frame, WinoApplicationMode mode) private void PruneInnerShellBackStackForMode(Frame frame, WinoApplicationMode mode)
@@ -636,7 +619,33 @@ public class NavigationService : NavigationServiceBase, INavigationService
frame.BackStack.Clear(); frame.BackStack.Clear();
frame.ForwardStack.Clear(); frame.ForwardStack.Clear();
UpdateCurrentModeBackStackState(frame); }
private bool CanGoBackInternal()
{
var innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
return _statePersistanceService.ApplicationMode switch
{
WinoApplicationMode.Mail => _statePersistanceService.IsReadingMail && _statePersistanceService.IsReaderNarrowed,
WinoApplicationMode.Settings => innerShellFrame?.Content is SettingsPage settingsPage && settingsPage.CanNavigateBack,
WinoApplicationMode.Calendar or WinoApplicationMode.Contacts => HasModeScopedBackStack(innerShellFrame, _statePersistanceService.ApplicationMode),
_ => false
};
}
private bool HasModeScopedBackStack(Frame? innerShellFrame, WinoApplicationMode mode)
{
if (innerShellFrame == null || innerShellFrame.BackStack.Count == 0)
return false;
for (int i = innerShellFrame.BackStack.Count - 1; i >= 0; i--)
{
if (IsPageTypeAllowedInMode(mode, innerShellFrame.BackStack[i].SourcePageType))
return true;
}
return false;
} }
// Standalone EML viewer. // Standalone EML viewer.
@@ -32,23 +32,12 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
private void ServicePropertyChanged(object? sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e?.PropertyName ?? string.Empty); private void ServicePropertyChanged(object? sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e?.PropertyName ?? string.Empty);
public bool IsBackButtonVisible =>
ApplicationMode == WinoApplicationMode.Mail
? (IsReadingMail && IsReaderNarrowed) || HasCurrentModeBackStack
: HasCurrentModeBackStack;
private WinoApplicationMode applicationMode = WinoApplicationMode.Mail; private WinoApplicationMode applicationMode = WinoApplicationMode.Mail;
public WinoApplicationMode ApplicationMode public WinoApplicationMode ApplicationMode
{ {
get => applicationMode; get => applicationMode;
set set => SetProperty(ref applicationMode, value);
{
if (SetProperty(ref applicationMode, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
} }
private bool isEventDetailsVisible; private bool isEventDetailsVisible;
@@ -60,40 +49,18 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
{ {
if (SetProperty(ref isEventDetailsVisible, value)) if (SetProperty(ref isEventDetailsVisible, value))
{ {
OnPropertyChanged(nameof(IsBackButtonVisible));
IsReaderNarrowed = value; IsReaderNarrowed = value;
IsReadingMail = value; IsReadingMail = value;
} }
} }
} }
private bool hasCurrentModeBackStack;
public bool HasCurrentModeBackStack
{
get => hasCurrentModeBackStack;
set
{
if (SetProperty(ref hasCurrentModeBackStack, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
}
private bool isReadingMail; private bool isReadingMail;
public bool IsReadingMail public bool IsReadingMail
{ {
get => isReadingMail; get => isReadingMail;
set set => SetProperty(ref isReadingMail, value);
{
if (SetProperty(ref isReadingMail, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
} }
private bool shouldShiftMailRenderingDesign; private bool shouldShiftMailRenderingDesign;
@@ -109,13 +76,7 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
public bool IsReaderNarrowed public bool IsReaderNarrowed
{ {
get => isReaderNarrowed; get => isReaderNarrowed;
set set => SetProperty(ref isReaderNarrowed, value);
{
if (SetProperty(ref isReaderNarrowed, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
} }
private string coreWindowTitle = string.Empty; private string coreWindowTitle = string.Empty;
+4 -1
View File
@@ -31,10 +31,13 @@
VerticalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
BackRequested="BackButtonClicked" BackRequested="BackButtonClicked"
Background="Transparent" Background="Transparent"
IsBackButtonVisible="{x:Bind StatePersistanceService.IsBackButtonVisible, Mode=OneWay}"
IsPaneToggleButtonVisible="True" IsPaneToggleButtonVisible="True"
PaneToggleRequested="PaneButtonClicked" PaneToggleRequested="PaneButtonClicked"
Subtitle="{x:Bind StatePersistanceService.CoreWindowTitle, Mode=OneWay}"> Subtitle="{x:Bind StatePersistanceService.CoreWindowTitle, Mode=OneWay}">
<TitleBar.Resources>
<HorizontalAlignment x:Key="TitleBarContentHorizontalAlignment">Stretch</HorizontalAlignment>
<VerticalAlignment x:Key="TitleBarContentVerticalAlignment">Stretch</VerticalAlignment>
</TitleBar.Resources>
<TitleBar.RightHeader> <TitleBar.RightHeader>
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<Button <Button
+40
View File
@@ -46,12 +46,14 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
private ICalendarShellClient? _activeCalendarClient; private ICalendarShellClient? _activeCalendarClient;
private readonly CalendarTitleBarContent _calendarTitleBarContent = new(); private readonly CalendarTitleBarContent _calendarTitleBarContent = new();
private long _calendarTypeSelectorChangedToken; private long _calendarTypeSelectorChangedToken;
private bool _isBackButtonVisibilityReady;
public ShellWindow() public ShellWindow()
{ {
RegisterRecipients(); RegisterRecipients();
InitializeComponent(); InitializeComponent();
StatePersistanceService.StatePropertyChanged += StatePersistenceServiceChanged;
MinWidth = 420; MinWidth = 420;
MinHeight = 420; MinHeight = 420;
@@ -138,7 +140,9 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
_ = StartCalendarReminderServerAsync(); _ = StartCalendarReminderServerAsync();
} }
_isBackButtonVisibilityReady = true;
ApplyTitleBarContent(); ApplyTitleBarContent();
RefreshBackButtonVisibility();
} }
private async Task StartCalendarReminderServerAsync() private async Task StartCalendarReminderServerAsync()
@@ -166,6 +170,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
public void Receive(TitleBarShellContentUpdated message) public void Receive(TitleBarShellContentUpdated message)
{ {
ApplyTitleBarContent(); ApplyTitleBarContent();
RefreshBackButtonVisibility();
} }
public void Receive(ApplicationThemeChanged message) public void Receive(ApplicationThemeChanged message)
@@ -279,6 +284,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
{ {
AttachCalendarClient(null); AttachCalendarClient(null);
ShellTitleBar.Content = MainShellFrame.Content is BasePage basePage ? basePage.ShellContent : null; ShellTitleBar.Content = MainShellFrame.Content is BasePage basePage ? basePage.ShellContent : null;
RefreshBackButtonVisibility();
return; return;
} }
@@ -288,10 +294,43 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
{ {
RefreshCalendarSelector(); RefreshCalendarSelector();
ShellTitleBar.Content = _calendarTitleBarContent; ShellTitleBar.Content = _calendarTitleBarContent;
RefreshBackButtonVisibility();
return; return;
} }
ShellTitleBar.Content = shellPage.GetShellFrame().Content is BasePage page ? page.ShellContent : null; ShellTitleBar.Content = shellPage.GetShellFrame().Content is BasePage page ? page.ShellContent : null;
RefreshBackButtonVisibility();
}
private void StatePersistenceServiceChanged(object? sender, string propertyName)
{
if (!DispatcherQueue.HasThreadAccess)
{
var enqueued = DispatcherQueue.TryEnqueue(() => StatePersistenceServiceChanged(sender, propertyName));
if (!enqueued)
throw new InvalidOperationException("Could not marshal shell state changes onto the UI thread.");
return;
}
if (propertyName == nameof(IStatePersistanceService.ApplicationMode) ||
propertyName == nameof(IStatePersistanceService.IsReadingMail) ||
propertyName == nameof(IStatePersistanceService.IsReaderNarrowed) ||
propertyName == nameof(IStatePersistanceService.IsEventDetailsVisible))
{
RefreshBackButtonVisibility();
}
}
private void RefreshBackButtonVisibility()
{
if (!_isBackButtonVisibilityReady)
{
ShellTitleBar.IsBackButtonVisible = false;
return;
}
ShellTitleBar.IsBackButtonVisible = NavigationService.CanGoBack();
} }
private void AttachCalendarClient(ICalendarShellClient? calendarClient) private void AttachCalendarClient(ICalendarShellClient? calendarClient)
@@ -390,6 +429,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
{ {
AppWindow.Closing -= OnAppWindowClosing; AppWindow.Closing -= OnAppWindowClosing;
AttachCalendarClient(null); AttachCalendarClient(null);
StatePersistanceService.StatePropertyChanged -= StatePersistenceServiceChanged;
_calendarTitleBarContent.UnregisterSelectedTypeChanged(_calendarTypeSelectorChangedToken); _calendarTitleBarContent.UnregisterSelectedTypeChanged(_calendarTypeSelectorChangedToken);
_calendarTitleBarContent.PreviousDateRequested -= CalendarTitleBarContentPreviousDateRequested; _calendarTitleBarContent.PreviousDateRequested -= CalendarTitleBarContentPreviousDateRequested;
_calendarTitleBarContent.NextDateRequested -= CalendarTitleBarContentNextDateRequested; _calendarTitleBarContent.NextDateRequested -= CalendarTitleBarContentNextDateRequested;
+3 -4
View File
@@ -77,9 +77,6 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
// Unregister frame navigation event // Unregister frame navigation event
SettingsFrame.Navigated -= SettingsFrameNavigated; SettingsFrame.Navigated -= SettingsFrameNavigated;
// Reset navigation state when leaving SettingsPage
ViewModel.StatePersistenceService.HasCurrentModeBackStack = false;
base.OnNavigatingFrom(e); base.OnNavigatingFrom(e);
} }
@@ -225,9 +222,11 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
private void UpdateBackNavigationState() private void UpdateBackNavigationState()
{ {
ViewModel.StatePersistenceService.HasCurrentModeBackStack = PageHistory.Count > 1 && SettingsFrame.CanGoBack; WeakReferenceMessenger.Default.Send(new TitleBarShellContentUpdated());
} }
public bool CanNavigateBack => PageHistory.Count > 1 && SettingsFrame.CanGoBack;
private async Task RefreshCurrentPageStateAsync() private async Task RefreshCurrentPageStateAsync()
{ {
var activePage = PageHistory.LastOrDefault()?.Request.PageType ?? WinoPage.SettingOptionsPage; var activePage = PageHistory.LastOrDefault()?.Request.PageType ?? WinoPage.SettingOptionsPage;
@@ -157,7 +157,6 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
private void ResetShellModeNavigationState() private void ResetShellModeNavigationState()
{ {
ViewModel.StatePersistenceService.HasCurrentModeBackStack = false;
InnerShellFrame.BackStack.Clear(); InnerShellFrame.BackStack.Clear();
InnerShellFrame.ForwardStack.Clear(); InnerShellFrame.ForwardStack.Clear();
} }