Skip to content

Commit 5a9c293

Browse files
Fix Xbox shell focus, search, and library filter navigation
1 parent 2940ce0 commit 5a9c293

30 files changed

Lines changed: 1004 additions & 64 deletions

src/JellyBox/App.xaml.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,22 @@ public App()
4040

4141
InitializeComponent();
4242

43+
UnhandledException += OnUnhandledException;
44+
4345
Current.RequiresPointerMode = ApplicationRequiresPointerMode.WhenRequested;
4446

4547
Suspending += OnSuspending;
4648
}
4749

50+
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
51+
{
52+
System.Diagnostics.Debug.WriteLine($"Unhandled UI exception: {e.Exception}");
53+
if (e.Exception.InnerException is not null)
54+
{
55+
System.Diagnostics.Debug.WriteLine($"Inner exception: {e.Exception.InnerException}");
56+
}
57+
}
58+
4859
/// <inheritdoc/>
4960
protected override void OnLaunched(LaunchActivatedEventArgs args)
5061
{

src/JellyBox/AppServices.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,16 @@ private AppServices()
6464
serviceCollection.AddSingleton<DeviceProfileManager>();
6565
serviceCollection.AddSingleton<JellyfinImageResolver>();
6666
serviceCollection.AddSingleton<NavigationManager>();
67+
serviceCollection.AddSingleton<ShellFocusCoordinator>();
6768

6869
// View Models
6970
serviceCollection.AddTransient<HomeViewModel>();
7071
serviceCollection.AddTransient<ItemDetailsViewModel>();
7172
serviceCollection.AddTransient<LibraryViewModel>();
7273
serviceCollection.AddTransient<LoginViewModel>();
7374
serviceCollection.AddTransient<MainPageViewModel>();
75+
serviceCollection.AddTransient<SearchViewModel>();
76+
serviceCollection.AddTransient<ShellSearchViewModel>();
7477
serviceCollection.AddTransient<ServerSelectionViewModel>();
7578
serviceCollection.AddTransient<VideoViewModel>();
7679
serviceCollection.AddTransient<WebVideoViewModel>();

src/JellyBox/Behaviors/FocusFirstItemBehavior.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ namespace JellyBox.Behaviors;
88
/// <summary>
99
/// Automatically focuses the first item in a list once items are loaded and containers are realized.
1010
/// </summary>
11+
#pragma warning disable CA1812 // Instantiated via XAML Interaction.Behaviors.
1112
internal sealed class FocusFirstItemBehavior : Behavior<ListViewBase>
13+
#pragma warning restore CA1812
1214
{
1315
private bool _hasFocused;
1416

@@ -41,4 +43,4 @@ private async void OnLayoutUpdated(object? sender, object e)
4143
AssociatedObject.LayoutUpdated -= OnLayoutUpdated;
4244
await FocusManager.TryFocusAsync(firstItem, FocusState.Programmatic);
4345
}
44-
}
46+
}

src/JellyBox/Behaviors/FocusOnLoadBehavior.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace JellyBox.Behaviors;
66

7+
#pragma warning disable CA1812 // Instantiated via XAML Interaction.Behaviors.
78
internal sealed class FocusOnLoadBehavior : Behavior<Control>
9+
#pragma warning restore CA1812
810
{
911
protected override void OnAttached()
1012
{

src/JellyBox/Behaviors/ListViewBaseCommandBehavior.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ namespace JellyBox.Behaviors;
77
/// <summary>
88
/// Invokes the NavigateCommand on INavigable items when they are clicked in a ListViewBase control.
99
/// </summary>
10+
#pragma warning disable CA1812 // Instantiated via XAML Interaction.Behaviors.
1011
internal sealed class ListViewBaseCommandBehavior : Behavior<ListViewBase>
12+
#pragma warning restore CA1812
1113
{
1214
protected override void OnAttached()
1315
{

src/JellyBox/Behaviors/ScrollOnFocusBehavior.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ namespace JellyBox.Behaviors;
1111
/// Adjusts scroll position to keep focused items within the TV-safe zone.
1212
/// Supports both horizontal carousels and full grid layouts.
1313
/// </summary>
14+
#pragma warning disable CA1812 // Instantiated via XAML Interaction.Behaviors.
1415
internal sealed class ScrollOnFocusBehavior : Behavior<ListViewBase>
16+
#pragma warning restore CA1812
1517
{
1618
private ScrollViewer? _scrollViewer;
1719

src/JellyBox/Behaviors/SectionNavigationBehavior.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using JellyBox;
2+
using JellyBox.Services;
3+
using Microsoft.Extensions.DependencyInjection;
14
using Microsoft.Xaml.Interactivity;
25
using Windows.Foundation;
36
using Windows.UI.Core;
@@ -11,9 +14,12 @@ namespace JellyBox.Behaviors;
1114
/// Handles vertical navigation between horizontal list rows within a sections container.
1215
/// Maintains horizontal position when moving between rows and handles edge trapping.
1316
/// </summary>
17+
#pragma warning disable CA1812 // Instantiated via XAML Interaction.Behaviors.
1418
internal sealed class SectionNavigationBehavior : Behavior<ItemsControl>
19+
#pragma warning restore CA1812
1520
{
1621
private ScrollViewer? _scrollViewer;
22+
private ShellFocusCoordinator? _shellFocus;
1723

1824
/// <summary>
1925
/// The ScrollViewer to use for bringing items into view.
@@ -55,6 +61,7 @@ protected override void OnDetaching()
5561
private void OnLoaded(object sender, RoutedEventArgs e)
5662
{
5763
_scrollViewer = ScrollViewer ?? AssociatedObject.FindAncestor<ScrollViewer>();
64+
_shellFocus ??= AppServices.Instance.ServiceProvider.GetRequiredService<ShellFocusCoordinator>();
5865
}
5966

6067
private void OnGotFocus(object sender, RoutedEventArgs e)
@@ -100,7 +107,7 @@ private void OnLosingFocus(UIElement sender, LosingFocusEventArgs e)
100107

101108
if (targetIndex < 0)
102109
{
103-
if (TrapAtTop)
110+
if (TrapAtTop && _shellFocus?.TryRedirectFocusToSearch(e) != true)
104111
{
105112
e.TryCancel();
106113
}

src/JellyBox/CancellableLoad.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ internal sealed class CancellableLoad
1515
{
1616
private CancellationTokenSource? _cts;
1717

18+
/// <summary>
19+
/// Cancels any in-flight load without starting a new one.
20+
/// </summary>
21+
public async Task CancelAsync()
22+
{
23+
CancellationTokenSource? previous = Interlocked.Exchange(ref _cts, null);
24+
if (previous is not null)
25+
{
26+
await previous.CancelAsync();
27+
}
28+
}
29+
1830
/// <summary>
1931
/// Cancels any prior in-flight load, then runs <paramref name="operation"/> with a fresh
2032
/// cancellation token. If this load is itself superseded by a later call, the resulting

src/JellyBox/Glyphs.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal static class Glyphs
77
{
88
// Navigation
99
public const string Home = "\uE80F";
10+
public const string Search = "\uE721";
1011
public const string Library = "\uE8F1";
1112
public const string Switch = "\uE895";
1213
public const string SignOut = "\uE8BB";

src/JellyBox/MainPage.xaml

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,71 @@
22
x:Class="JellyBox.MainPage"
33
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:models="using:JellyBox.Models"
6+
xmlns:g="using:JellyBox"
57
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
68
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
79
mc:Ignorable="d"
810
Background="{StaticResource BackgroundBase}">
911

1012
<Grid XYFocusKeyboardNavigation="Enabled">
11-
<Frame x:Name="ContentFrame" />
13+
<Grid.RowDefinitions>
14+
<RowDefinition Height="Auto" />
15+
<RowDefinition Height="*" />
16+
</Grid.RowDefinitions>
17+
18+
<Grid
19+
Grid.Row="0"
20+
Padding="48,20,48,12"
21+
Background="{StaticResource BackgroundBase}"
22+
XYFocusKeyboardNavigation="Enabled">
23+
<AutoSuggestBox
24+
x:Name="SearchBox"
25+
MaxWidth="720"
26+
HorizontalAlignment="Stretch"
27+
IsTabStop="False"
28+
PlaceholderText="Search movies and TV shows"
29+
Style="{StaticResource SearchAutoSuggestBox}"
30+
ItemsSource="{x:Bind ViewModel.Search.Suggestions, Mode=OneWay}"
31+
TextChanged="SearchBox_TextChanged"
32+
QuerySubmitted="SearchBox_QuerySubmitted"
33+
SuggestionChosen="SearchBox_SuggestionChosen"
34+
LostFocus="SearchBox_LostFocus"
35+
XYFocusKeyboardNavigation="Enabled">
36+
<AutoSuggestBox.QueryIcon>
37+
<SymbolIcon Symbol="Find" />
38+
</AutoSuggestBox.QueryIcon>
39+
<AutoSuggestBox.ItemTemplate>
40+
<DataTemplate x:DataType="models:SearchSuggestion">
41+
<StackPanel Orientation="Horizontal" Spacing="12" Padding="4,10">
42+
<FontIcon
43+
FontFamily="{StaticResource SegoeIcons}"
44+
Glyph="{x:Bind g:Glyphs.Search}"
45+
FontSize="16"
46+
Foreground="{StaticResource TextMuted}"
47+
VerticalAlignment="Center" />
48+
<StackPanel Spacing="2" VerticalAlignment="Center">
49+
<TextBlock
50+
Text="{x:Bind DisplayText}"
51+
FontSize="{StaticResource FontS}"
52+
Foreground="{StaticResource TextPrimary}" />
53+
<TextBlock
54+
Text="{x:Bind SecondaryText}"
55+
FontSize="{StaticResource FontXS}"
56+
Foreground="{StaticResource TextMuted}" />
57+
</StackPanel>
58+
</StackPanel>
59+
</DataTemplate>
60+
</AutoSuggestBox.ItemTemplate>
61+
</AutoSuggestBox>
62+
</Grid>
63+
64+
<Frame x:Name="ContentFrame" Grid.Row="1" />
1265

1366
<Grid
1467
x:Name="NavigationOverlay"
68+
Grid.Row="0"
69+
Grid.RowSpan="2"
1570
Visibility="Collapsed"
1671
XYFocusKeyboardNavigation="Enabled">
1772

0 commit comments

Comments
 (0)