diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8a4df3..ad5a735 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: runs-on: windows-2025-vs2026 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 # Git Versioning requires a non-shallow clone diff --git a/Directory.Packages.props b/Directory.Packages.props index e711718..903e1d1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,17 +7,17 @@ - - - - - + + + + + - + - + \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..78a4074 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file 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/JellyBox.csproj b/src/JellyBox/JellyBox.csproj index 1225523..244bcf2 100644 --- a/src/JellyBox/JellyBox.csproj +++ b/src/JellyBox/JellyBox.csproj @@ -41,7 +41,8 @@ + DependsOnTargets="GetBuildVersion" + Condition="'$(DesignTimeBuild)' != 'true' and '$(BuildingProject)' == 'true'"> <_OriginalAppxManifest Include="@(AppxManifest)" /> 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 @@ + +