Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 7 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
<ItemGroup>
<PackageVersion Include="Blurhash.Core" Version="4.0.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Jellyfin.Sdk" Version="2025.10.21" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.7" />
<PackageVersion Include="Jellyfin.Sdk" Version="2026.6.19-unstable.202606190139" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.9" />
<PackageVersion Include="Microsoft.UI.Xaml" Version="2.8.7" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3912.50" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.4022.49" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Uwp.Managed" Version="3.0.1" />
</ItemGroup>
<ItemGroup>
<!-- Versioning (https://github.com/dotnet/Nerdbank.GitVersioning) -->
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" PrivateAssets="all" />
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.10.85" PrivateAssets="all" />
</ItemGroup>
</Project>
15 changes: 15 additions & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config />
<packageRestore>
<add key="enabled" value="True" />
<add key="automatic" value="True" />
</packageRestore>
<activePackageSource>
<add key="All" value="(Aggregate source)" />
</activePackageSource>
<packageSources>
<clear />
<add key="NuGet.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
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
3 changes: 2 additions & 1 deletion src/JellyBox/JellyBox.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
</ItemGroup>
<Target Name="SetAppxManifestVersion"
BeforeTargets="PrepareForBuild"
DependsOnTargets="GetBuildVersion">
DependsOnTargets="GetBuildVersion"
Condition="'$(DesignTimeBuild)' != 'true' and '$(BuildingProject)' == 'true'">
<ItemGroup>
<_OriginalAppxManifest Include="@(AppxManifest)" />
<AppxManifest Remove="@(AppxManifest)" />
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
Loading