diff --git a/src/JellyBox/App.xaml.cs b/src/JellyBox/App.xaml.cs
index bce1ebf..76ff2b4 100644
--- a/src/JellyBox/App.xaml.cs
+++ b/src/JellyBox/App.xaml.cs
@@ -40,11 +40,22 @@ public App()
InitializeComponent();
+ UnhandledException += OnUnhandledException;
+
Current.RequiresPointerMode = ApplicationRequiresPointerMode.WhenRequested;
Suspending += OnSuspending;
}
+ private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
+ {
+ System.Diagnostics.Debug.WriteLine($"Unhandled UI exception: {e.Exception}");
+ if (e.Exception.InnerException is not null)
+ {
+ System.Diagnostics.Debug.WriteLine($"Inner exception: {e.Exception.InnerException}");
+ }
+ }
+
///
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
diff --git a/src/JellyBox/AppServices.cs b/src/JellyBox/AppServices.cs
index 219ee60..fcc24c1 100644
--- a/src/JellyBox/AppServices.cs
+++ b/src/JellyBox/AppServices.cs
@@ -71,6 +71,8 @@ private AppServices()
serviceCollection.AddTransient();
serviceCollection.AddTransient();
serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
serviceCollection.AddTransient();
serviceCollection.AddTransient();
serviceCollection.AddTransient();
diff --git a/src/JellyBox/Glyphs.cs b/src/JellyBox/Glyphs.cs
index 0f946f3..e98e90f 100644
--- a/src/JellyBox/Glyphs.cs
+++ b/src/JellyBox/Glyphs.cs
@@ -7,6 +7,7 @@ internal static class Glyphs
{
// Navigation
public const string Home = "\uE80F";
+ public const string Search = "\uE721";
public const string Library = "\uE8F1";
public const string Switch = "\uE895";
public const string SignOut = "\uE8BB";
diff --git a/src/JellyBox/MainPage.xaml b/src/JellyBox/MainPage.xaml
index 39c9a15..718dfad 100644
--- a/src/JellyBox/MainPage.xaml
+++ b/src/JellyBox/MainPage.xaml
@@ -2,16 +2,68 @@
x:Class="JellyBox.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:models="using:JellyBox.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{StaticResource BackgroundBase}">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/JellyBox/MainPage.xaml.cs b/src/JellyBox/MainPage.xaml.cs
index ac0c745..d8a1d8d 100644
--- a/src/JellyBox/MainPage.xaml.cs
+++ b/src/JellyBox/MainPage.xaml.cs
@@ -1,6 +1,7 @@
+using System.ComponentModel;
+using JellyBox.Models;
using JellyBox.ViewModels;
using Microsoft.Extensions.DependencyInjection;
-using Windows.System;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
@@ -12,6 +13,8 @@ namespace JellyBox;
internal sealed partial class MainPage : Page
{
private FrameworkElement? _lastFocusedElement;
+ private bool _ignoreNextQuerySubmitted;
+ private bool _suppressSearchTextSync;
public MainPage()
{
@@ -19,12 +22,11 @@ public MainPage()
ViewModel = AppServices.Instance.ServiceProvider.GetRequiredService();
ViewModel.IsMenuOpenChanged += OnIsMenuOpenChanged;
+ ViewModel.Search.PropertyChanged += OnSearchPropertyChanged;
// Cache the page state so the ContentFrame's BackStack can be preserved
NavigationCacheMode = NavigationCacheMode.Required;
- KeyDown += OnKeyDown;
-
SlideInAnimation.Completed += SlideInCompleted;
Loaded += (sender, e) =>
@@ -36,6 +38,7 @@ public MainPage()
Unloaded += (sender, e) =>
{
ContentFrame.Navigated -= ContentFrameNavigated;
+ ViewModel.Search.PropertyChanged -= OnSearchPropertyChanged;
};
}
@@ -114,86 +117,71 @@ private void CloseNavigation(object sender, TappedRoutedEventArgs e)
ViewModel.CloseNavigationCommand.Execute(null);
}
- ///
- /// Keyboard and gamepad input handling.
- /// Only handles commands and nav-menu-specific logic.
- /// Directional focus movement is handled by XYFocusKeyboardNavigation in XAML.
- ///
- private void OnKeyDown(object sender, KeyRoutedEventArgs e)
+ private void OnSearchPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- // Don't intercept keys when a text input control has focus
- if (FocusManager.GetFocusedElement() is TextBox or PasswordBox)
+ if (_suppressSearchTextSync
+ || e.PropertyName is not nameof(ShellSearchViewModel.Query)
+ || SearchBox.Text == ViewModel.Search.Query)
{
return;
}
- switch (e.Key)
+ SearchBox.Text = ViewModel.Search.Query;
+ }
+
+ private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
+ {
+ if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
- // Back gesture - close navigation if open
- case VirtualKey.Back:
- case VirtualKey.GamepadB:
- {
- if (ViewModel.IsMenuOpen)
- {
- ViewModel.CloseNavigationCommand.Execute(null);
- e.Handled = true;
- }
+ ViewModel.Search.Query = sender.Text ?? string.Empty;
+ }
+ }
- break;
- }
+ private void SearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
+ {
+ if (_ignoreNextQuerySubmitted)
+ {
+ _ignoreNextQuerySubmitted = false;
+ return;
+ }
- // Toggle navigation menu
- case VirtualKey.GamepadMenu:
- case VirtualKey.GamepadView:
- case VirtualKey.M:
- {
- ViewModel.ToggleNavigationCommand.Execute(null);
- e.Handled = true;
- break;
- }
+ if (args.ChosenSuggestion is SearchSuggestion suggestion)
+ {
+ OpenSearchSuggestion(suggestion);
+ return;
+ }
- // Close navigation menu
- case VirtualKey.Escape:
- {
- if (ViewModel.IsMenuOpen)
- {
- ViewModel.CloseNavigationCommand.Execute(null);
- e.Handled = true;
- }
+ ViewModel.Search.SubmitQuery(args.QueryText);
+ }
- break;
- }
+ private void SearchBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
+ {
+ if (args.SelectedItem is SearchSuggestion suggestion)
+ {
+ OpenSearchSuggestion(suggestion);
+ }
+ }
- // Right closes nav menu when open
- case VirtualKey.Right:
- case VirtualKey.GamepadDPadRight:
- case VirtualKey.GamepadLeftThumbstickRight:
- case VirtualKey.NavigationRight:
- {
- if (ViewModel.IsMenuOpen)
- {
- ViewModel.CloseNavigationCommand.Execute(null);
- e.Handled = true;
- }
+ private void OpenSearchSuggestion(SearchSuggestion suggestion)
+ {
+ _ignoreNextQuerySubmitted = true;
+ ViewModel.Search.PrepareSuggestionNavigation();
+ ViewModel.Search.ClearSuggestions();
- break;
+ _ = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
+ {
+ _suppressSearchTextSync = true;
+ try
+ {
+ ViewModel.Search.SetQueryText(suggestion.DisplayText);
+ SearchBox.Text = suggestion.DisplayText;
+ ViewModel.Search.NavigateToItem(suggestion.ItemId);
}
-
- // Left at edge opens nav menu
- case VirtualKey.Left:
- case VirtualKey.GamepadDPadLeft:
- case VirtualKey.GamepadLeftThumbstickLeft:
- case VirtualKey.NavigationLeft:
+ finally
{
- if (!ViewModel.IsMenuOpen && !FocusManager.TryMoveFocus(FocusNavigationDirection.Left))
- {
- ViewModel.OpenNavigationCommand.Execute(null);
- e.Handled = true;
- }
-
- break;
+ _suppressSearchTextSync = false;
}
- }
+ });
}
internal sealed record Parameters(Action DeferredNavigationAction);
diff --git a/src/JellyBox/Models/SearchSuggestion.cs b/src/JellyBox/Models/SearchSuggestion.cs
new file mode 100644
index 0000000..5abea04
--- /dev/null
+++ b/src/JellyBox/Models/SearchSuggestion.cs
@@ -0,0 +1,3 @@
+namespace JellyBox.Models;
+
+internal sealed record SearchSuggestion(string DisplayText, string? SecondaryText, Guid ItemId);
\ No newline at end of file
diff --git a/src/JellyBox/Resources/Styles.xaml b/src/JellyBox/Resources/Styles.xaml
index ac596c0..edda3a3 100644
--- a/src/JellyBox/Resources/Styles.xaml
+++ b/src/JellyBox/Resources/Styles.xaml
@@ -1008,6 +1008,16 @@
+
+