Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/JellyBox/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
}

/// <inheritdoc/>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
Expand Down
2 changes: 2 additions & 0 deletions src/JellyBox/AppServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ private AppServices()
serviceCollection.AddTransient<LibraryViewModel>();
serviceCollection.AddTransient<LoginViewModel>();
serviceCollection.AddTransient<MainPageViewModel>();
serviceCollection.AddTransient<SearchViewModel>();
serviceCollection.AddTransient<ShellSearchViewModel>();
serviceCollection.AddTransient<ServerSelectionViewModel>();
serviceCollection.AddTransient<VideoViewModel>();
serviceCollection.AddTransient<WebVideoViewModel>();
Expand Down
1 change: 1 addition & 0 deletions src/JellyBox/Glyphs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
54 changes: 53 additions & 1 deletion src/JellyBox/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}">

<Grid XYFocusKeyboardNavigation="Enabled">
<Frame x:Name="ContentFrame" />
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<Grid
Grid.Row="0"
Padding="48,20,48,12"
Background="{StaticResource BackgroundBase}"
XYFocusKeyboardNavigation="Enabled">
<AutoSuggestBox
x:Name="SearchBox"
MaxWidth="720"
HorizontalAlignment="Stretch"
PlaceholderText="Search movies and TV shows"
Style="{StaticResource SearchAutoSuggestBox}"
ItemsSource="{x:Bind ViewModel.Search.Suggestions, Mode=OneWay}"
TextChanged="SearchBox_TextChanged"
QuerySubmitted="SearchBox_QuerySubmitted"
SuggestionChosen="SearchBox_SuggestionChosen"
XYFocusKeyboardNavigation="Enabled">
<AutoSuggestBox.QueryIcon>
<SymbolIcon Symbol="Find" />
</AutoSuggestBox.QueryIcon>
<AutoSuggestBox.ItemTemplate>
<DataTemplate x:DataType="models:SearchSuggestion">
<StackPanel Orientation="Horizontal" Spacing="12" Padding="4,10">
<FontIcon
FontFamily="{StaticResource SegoeIcons}"
Glyph="&#xE721;"
FontSize="16"
Foreground="{StaticResource TextMuted}"
VerticalAlignment="Center" />
<StackPanel Spacing="2" VerticalAlignment="Center">
<TextBlock
Text="{x:Bind DisplayText}"
FontSize="{StaticResource FontS}"
Foreground="{StaticResource TextPrimary}" />
<TextBlock
Text="{x:Bind SecondaryText}"
FontSize="{StaticResource FontXS}"
Foreground="{StaticResource TextMuted}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</AutoSuggestBox.ItemTemplate>
</AutoSuggestBox>
</Grid>

<Frame x:Name="ContentFrame" Grid.Row="1" />

<Grid
x:Name="NavigationOverlay"
Grid.Row="0"
Grid.RowSpan="2"
Visibility="Collapsed"
XYFocusKeyboardNavigation="Enabled">

Expand Down
124 changes: 56 additions & 68 deletions src/JellyBox/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,19 +13,20 @@ namespace JellyBox;
internal sealed partial class MainPage : Page
{
private FrameworkElement? _lastFocusedElement;
private bool _ignoreNextQuerySubmitted;
private bool _suppressSearchTextSync;

public MainPage()
{
InitializeComponent();

ViewModel = AppServices.Instance.ServiceProvider.GetRequiredService<MainPageViewModel>();
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) =>
Expand All @@ -36,6 +38,7 @@ public MainPage()
Unloaded += (sender, e) =>
{
ContentFrame.Navigated -= ContentFrameNavigated;
ViewModel.Search.PropertyChanged -= OnSearchPropertyChanged;
};
}

Expand Down Expand Up @@ -114,86 +117,71 @@ private void CloseNavigation(object sender, TappedRoutedEventArgs e)
ViewModel.CloseNavigationCommand.Execute(null);
}

/// <summary>
/// Keyboard and gamepad input handling.
/// Only handles commands and nav-menu-specific logic.
/// Directional focus movement is handled by XYFocusKeyboardNavigation in XAML.
/// </summary>
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);
Expand Down
3 changes: 3 additions & 0 deletions src/JellyBox/Models/SearchSuggestion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace JellyBox.Models;

internal sealed record SearchSuggestion(string DisplayText, string? SecondaryText, Guid ItemId);
10 changes: 10 additions & 0 deletions src/JellyBox/Resources/Styles.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,16 @@
</Setter>
</Style>

<Style x:Key="SearchAutoSuggestBox" TargetType="AutoSuggestBox">
<Setter Property="MinHeight" Value="48" />
<Setter Property="Foreground" Value="{StaticResource TextPrimary}" />
<Setter Property="FontSize" Value="{StaticResource FontM}" />
<Setter Property="UseSystemFocusVisuals" Value="True" />
<Setter Property="FocusVisualPrimaryBrush" Value="{StaticResource FocusBorder}" />
<Setter Property="FocusVisualSecondaryBrush" Value="Transparent" />
<Setter Property="FocusVisualPrimaryThickness" Value="3" />
</Style>

<Style x:Key="PrimaryTextBox" TargetType="TextBox">
<Setter Property="MinWidth" Value="{ThemeResource TextControlThemeMinWidth}" />
<Setter Property="MinHeight" Value="40" />
Expand Down
8 changes: 4 additions & 4 deletions src/JellyBox/Resources/TransportControlsStyles.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
Style="{StaticResource TransportButtonStyle}"
Label="Rewind"
Command="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=RewindCommand}"
ToolTipService.ToolTip="Rewind 10 seconds (LB)"
ToolTipService.ToolTip="Rewind 10 seconds (LB / D-pad left)"
XYFocusKeyboardNavigation="Enabled">
<AppBarButton.Icon>
<FontIcon Glyph="&#xED3C;" FontFamily="Segoe MDL2 Assets" />
Expand All @@ -127,7 +127,7 @@
Style="{StaticResource TransportButtonStyle}"
Label="Play"
Command="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=PlayPauseCommand}"
ToolTipService.ToolTip="Play/Pause (Space)"
ToolTipService.ToolTip="Play/Pause (A / Menu / Space)"
XYFocusKeyboardNavigation="Enabled">
<AppBarButton.Icon>
<FontIcon x:Name="CustomPlayPauseIcon" Glyph="&#xE768;" FontFamily="Segoe MDL2 Assets" />
Expand All @@ -139,7 +139,7 @@
Style="{StaticResource TransportButtonStyle}"
Label="Fast Forward"
Command="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=FastForwardCommand}"
ToolTipService.ToolTip="Fast forward 30 seconds (RB)"
ToolTipService.ToolTip="Fast forward 30 seconds (RB / D-pad right)"
XYFocusKeyboardNavigation="Enabled">
<AppBarButton.Icon>
<FontIcon Glyph="&#xED3D;" FontFamily="Segoe MDL2 Assets" />
Expand Down Expand Up @@ -172,7 +172,7 @@
<Button x:Name="CustomVolumeButton"
Style="{StaticResource TransportIconButtonStyle}"
Command="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ToggleMuteCommand}"
ToolTipService.ToolTip="Toggle mute (M)"
ToolTipService.ToolTip="Toggle mute (X / M)"
XYFocusKeyboardNavigation="Enabled">
<FontIcon x:Name="CustomVolumeIcon" Glyph="&#xE767;" FontFamily="Segoe MDL2 Assets" />
<Button.Flyout>
Expand Down
Loading