Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/JellyBox/Behaviors/FocusFirstItemBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private async void OnLayoutUpdated(object? sender, object e)
}

_hasFocused = true;
AssociatedObject.LayoutUpdated -= OnLayoutUpdated;
await FocusManager.TryFocusAsync(firstItem, FocusState.Programmatic);
}
}
10 changes: 8 additions & 2 deletions src/JellyBox/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ private void SlideOutCompleted(object sender, object e)
{
NavigationOverlay.Visibility = Visibility.Collapsed;

// Restore focus to previously focused element
if (_lastFocusedElement is Control control)
// Restore focus to previously focused element (if it's still in the visual tree)
if (_lastFocusedElement is Control control && control.IsLoaded)
{
control.Focus(FocusState.Keyboard);
}
Expand Down Expand Up @@ -121,6 +121,12 @@ private void CloseNavigation(object sender, TappedRoutedEventArgs e)
/// </summary>
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
// Don't intercept keys when a text input control has focus
if (FocusManager.GetFocusedElement() is TextBox or PasswordBox)
{
return;
}

switch (e.Key)
{
// Back gesture - close navigation if open
Expand Down
13 changes: 12 additions & 1 deletion src/JellyBox/Services/NavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Windows.UI.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

namespace JellyBox.Services;

Expand All @@ -15,6 +16,8 @@ internal sealed class NavigationManager
// Fake item id used to identify the home page
public static readonly Guid HomeId = new Guid("CDF95D47-90C2-4057-B12C-BA81C34F2CB9");

public event Action? MenuOpenRequested;

private Frame _appFrame = null!; // TODO

private Frame? _contentFrame;
Expand Down Expand Up @@ -69,6 +72,8 @@ public void ClearHistory()

public void NavigateToLogin() => NavigateAppFrame<Login>();

public void RequestOpenMenu() => MenuOpenRequested?.Invoke();

public void NavigateToHome()
{
CurrentItem = HomeId;
Expand Down Expand Up @@ -148,7 +153,7 @@ private void NavigateContentFrame<TPage>(object? parameter = null)

private static void NavigateFrame<TPage>(Frame frame, ref object? currentParameter, object? parameter = null)
{
// Only navigate if the selected page isn't currently loaded.
// Only navigate if the selected page isn't currently loaded with the same parameter.
Type pageType = typeof(TPage);
if (frame.CurrentSourcePageType == pageType && Equals(currentParameter, parameter))
{
Expand Down Expand Up @@ -230,6 +235,12 @@ private void AcceleratorKeyActivated(CoreDispatcher sender, AcceleratorKeyEventA
return;
}

// Don't intercept navigation keys when a text input control has focus
if (FocusManager.GetFocusedElement() is TextBox or PasswordBox)
{
return;
}

CoreWindow coreWindow = Window.Current.CoreWindow;
CoreVirtualKeyStates downState = CoreVirtualKeyStates.Down;
bool menuKey = (coreWindow.GetKeyState(VirtualKey.Menu) & downState) == downState;
Expand Down
2 changes: 2 additions & 0 deletions src/JellyBox/ViewModels/ItemDetailsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ private void DetermineSubtitleOptions(MediaSourceInfo mediaSourceInfo)
SelectedSubtitleStream = selectedOption;
}

public void RequestOpenMenu() => _navigationManager.RequestOpenMenu();

public void Play()
{
// TODO: Support playlists
Expand Down
31 changes: 24 additions & 7 deletions src/JellyBox/ViewModels/MainPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ internal sealed partial class MainPageViewModel : ObservableObject
private readonly JellyfinApiClient _jellyfinApiClient;
private readonly NavigationManager _navigationManager;

private bool _isUpdatingSelection;

[ObservableProperty]
public partial bool IsMenuOpen { get; set; }

Expand All @@ -32,8 +34,7 @@ public MainPageViewModel(
_appSettings = appSettings;
_jellyfinApiClient = jellyfinApiClient;
_navigationManager = navigationManager;

_ = InitializeNavigationItemsAsync();
_navigationManager.MenuOpenRequested += () => IsMenuOpen = true;
}

[RelayCommand]
Expand All @@ -49,6 +50,8 @@ public void HandleParameters(MainPage.Parameters? parameters, Frame contentFrame
{
_navigationManager.RegisterContentFrame(contentFrame);

_ = InitializeNavigationItemsAsync();

if (parameters is not null)
{
parameters.DeferredNavigationAction();
Expand All @@ -62,6 +65,12 @@ public void HandleParameters(MainPage.Parameters? parameters, Frame contentFrame

public void NavigationItemSelected(NavigationView _, NavigationViewItemInvokedEventArgs args)
{
// Setting IsSelected programmatically can trigger ItemInvoked in UWP; ignore those.
if (_isUpdatingSelection)
{
return;
}

if (args.InvokedItemContainer?.Tag is NavigationViewItemContext context)
{
context.NavigateAction();
Expand All @@ -79,17 +88,25 @@ public void UpdateSelectedMenuItem()

if (NavigationItems is not null)
{
foreach (NavigationViewItemBase item in NavigationItems)
_isUpdatingSelection = true;
try
{
if (item.Tag is NavigationViewItemContext context)
foreach (NavigationViewItemBase item in NavigationItems)
{
if (context.ItemId == currentItem)
if (item.Tag is NavigationViewItemContext context)
{
item.IsSelected = true;
break;
if (context.ItemId == currentItem)
{
item.IsSelected = true;
break;
}
}
}
}
finally
{
_isUpdatingSelection = false;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/JellyBox/Views/Home.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ScrollViewer
x:Name="ContentScrollViewer"
VerticalScrollBarVisibility="Hidden"
BringIntoViewOnFocusChange="False"
HorizontalScrollMode="Disabled">
<ItemsControl
x:Name="SectionsControl"
Expand Down
1 change: 1 addition & 0 deletions src/JellyBox/Views/Home.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ private async void SectionsControl_LayoutUpdated(object? sender, object e)
}

_hasFocusedFirstItem = true;
SectionsControl.LayoutUpdated -= SectionsControl_LayoutUpdated;
await FocusManager.TryFocusAsync(firstItem, FocusState.Programmatic);
}
}
6 changes: 2 additions & 4 deletions src/JellyBox/Views/ItemDetails.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,7 @@
x:Name="PlayButton"
Style="{StaticResource PrimaryButton}"
Click="{x:Bind ViewModel.Play}"
Visibility="{x:Bind ViewModel.CanPlay, Mode=OneWay}"
XYFocusLeft="{x:Bind PlayButton}">
Visibility="{x:Bind ViewModel.CanPlay, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon Glyph="&#xE768;" FontSize="20" />
<TextBlock Text="Play" />
Expand Down Expand Up @@ -196,8 +195,7 @@
x:Name="FavoriteButton"
Style="{StaticResource IconButton}"
Click="{x:Bind ViewModel.ToggleFavorite}"
Visibility="{x:Bind ViewModel.CanMarkFavorite, Mode=OneWay}"
XYFocusRight="{x:Bind FavoriteButton}">
Visibility="{x:Bind ViewModel.CanMarkFavorite, Mode=OneWay}">
<FontIcon
Glyph="{x:Bind g:Glyphs.HeartFilled}"
FontFamily="{StaticResource SegoeIcons}"
Expand Down
17 changes: 17 additions & 0 deletions src/JellyBox/Views/ItemDetails.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ private void ContentInfoPanel_GotFocus(object sender, RoutedEventArgs e)
/// </summary>
private void ContentGrid_LosingFocus(UIElement sender, LosingFocusEventArgs e)
{
// Trap focus at left/right edges of action buttons so left opens nav menu
if (e.Direction is FocusNavigationDirection.Left or FocusNavigationDirection.Right
&& IsActionButton(e.OldFocusedElement)
&& !IsActionButton(e.NewFocusedElement))
{
e.TryCancel();

// XY focus consumes the key even when cancelled, so MainPage.OnKeyDown never fires.
// Open the nav menu directly for left-edge presses.
if (e.Direction == FocusNavigationDirection.Left)
{
ViewModel.RequestOpenMenu();
}

return;
}

if (e.Direction is not (FocusNavigationDirection.Up or FocusNavigationDirection.Down))
{
return;
Expand Down
7 changes: 5 additions & 2 deletions src/JellyBox/Views/Login.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
mc:Ignorable="d"
Background="{StaticResource BackgroundBase}">

<Grid Padding="0,48,0,0">
<Grid Padding="0,48,0,0" XYFocusKeyboardNavigation="Enabled">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="640">
<TextBlock
Text="Please sign in"
Foreground="White"
Foreground="{StaticResource TextPrimary}"
FontSize="{StaticResource FontL}"
Margin="0 0 0 28"
HorizontalAlignment="Center" />
Expand Down Expand Up @@ -51,20 +51,23 @@
Command="{x:Bind ViewModel.SignInCommand}"
IsEnabled="{x:Bind ViewModel.IsInteractable, Mode=OneWay}"
Style="{StaticResource PrimaryButton}"
HorizontalAlignment="Stretch"
Margin="0 0 0 10" />
<Button
Content="Use Quick Connect"
Visibility="{x:Bind ViewModel.IsQuickConnectEnabled, Mode=OneWay}"
Command="{x:Bind ViewModel.QuickConnectCommand}"
IsEnabled="{x:Bind ViewModel.IsInteractable, Mode=OneWay}"
Style="{StaticResource SecondaryButton}"
HorizontalAlignment="Stretch"
Margin="0 0 0 10" />
<!-- TODO: "Forgot Password" button -->
<Button
Content="Change Server"
Command="{x:Bind ViewModel.ChangeServerCommand}"
IsEnabled="{x:Bind ViewModel.IsInteractable, Mode=OneWay}"
Style="{StaticResource SecondaryButton}"
HorizontalAlignment="Stretch"
Margin="0 0 0 10" />
</StackPanel>
</Grid>
Expand Down
13 changes: 13 additions & 0 deletions src/JellyBox/Views/Login.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using JellyBox.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Windows.System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Navigation;

namespace JellyBox.Views;
Expand All @@ -12,6 +14,7 @@ public Login()
InitializeComponent();

ViewModel = AppServices.Instance.ServiceProvider.GetRequiredService<LoginViewModel>();
KeyDown += OnKeyDown;
}

protected override void OnNavigatedTo(NavigationEventArgs e)
Expand All @@ -27,4 +30,14 @@ protected override void OnNavigatedFrom(NavigationEventArgs e)
}

public LoginViewModel ViewModel { get; }

private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key is VirtualKey.Enter or VirtualKey.GamepadMenu
&& ViewModel.SignInCommand.CanExecute(null))
{
ViewModel.SignInCommand.Execute(null);
e.Handled = true;
}
}
}
7 changes: 4 additions & 3 deletions src/JellyBox/Views/ServerSelection.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
mc:Ignorable="d"
Background="{StaticResource BackgroundBase}">

<Grid>
<Grid XYFocusKeyboardNavigation="Enabled">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Top"
Expand Down Expand Up @@ -38,12 +38,13 @@
TextAlignment="Center"
TextWrapping="Wrap"
FontSize="{StaticResource FontM}"
Margin="1 1 1 20"
Margin="0,8,0,8"
Foreground="#CF4A4A" />
<Button
Content="Connect"
Command="{x:Bind ViewModel.ConnectCommand}"
Style="{StaticResource PrimaryButton}" />
Style="{StaticResource PrimaryButton}"
HorizontalAlignment="Stretch" />
</StackPanel>
</Grid>
</Page>
13 changes: 13 additions & 0 deletions src/JellyBox/Views/ServerSelection.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using JellyBox.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Windows.System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

namespace JellyBox.Views;

Expand All @@ -11,7 +13,18 @@ public ServerSelection()
InitializeComponent();

ViewModel = AppServices.Instance.ServiceProvider.GetRequiredService<ServerSelectionViewModel>();
KeyDown += OnKeyDown;
}

public ServerSelectionViewModel ViewModel { get; }

private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key is VirtualKey.Enter or VirtualKey.GamepadMenu
&& ViewModel.ConnectCommand.CanExecute(null))
{
ViewModel.ConnectCommand.Execute(null);
e.Handled = true;
}
}
}