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
17 changes: 15 additions & 2 deletions src/JellyBox/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,27 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)

_navigationManager.Initialize(rootFrame);

// TODO: Do properly async. Or defer until it's needed?
Task.Run(_deviceProfileManager.InitializeAsync);
// Initialize the device profile asynchronously. If this fails, playback features may not work correctly.
_ = InitializeDeviceProfileAsync();

// Ensure the current window is active
Window.Current.Activate();
}
}

private async Task InitializeDeviceProfileAsync()
{
try
{
await _deviceProfileManager.InitializeAsync();
}
catch (Exception ex)
{
// Log initialization failure but don't crash the app - playback will handle missing profile gracefully
System.Diagnostics.Debug.WriteLine($"Failed to initialize device profile: {ex}");
}
}

/// <summary>
/// Invoked when Navigation to a certain page fails.
/// </summary>
Expand Down
16 changes: 12 additions & 4 deletions src/JellyBox/Behaviors/FocusFirstItemOnLoadBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,19 @@ private void DataContextChanged(FrameworkElement sender, DataContextChangedEvent

private async void CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// TODO: This happens *every* time an item is added. How can we wait until it's stable?
DependencyObject firstItem = AssociatedObject.ContainerFromIndex(0);
if (firstItem is not null)
try
{
await FocusManager.TryFocusAsync(firstItem, FocusState.Programmatic);
// TODO: This happens *every* time an item is added. How can we wait until it's stable?
DependencyObject firstItem = AssociatedObject.ContainerFromIndex(0);
if (firstItem is not null)
{
await FocusManager.TryFocusAsync(firstItem, FocusState.Programmatic);
}
}
catch (Exception ex)
{
// Prevent app crash from async void event handler
System.Diagnostics.Debug.WriteLine($"Error in CollectionChanged: {ex}");
}
}
}
8 changes: 3 additions & 5 deletions src/JellyBox/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using JellyBox.Services;
using JellyBox.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Windows.UI.Core;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Navigation;
Expand Down Expand Up @@ -48,11 +47,10 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
base.OnNavigatedTo(e);
}

private async void ContentFrameNavigated(object sender, NavigationEventArgs e)
private void ContentFrameNavigated(object sender, NavigationEventArgs e)
{
// Update the selected item when a page navigation occurs in the body frame
await Dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
_ = Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
ViewModel.IsMenuOpen = false;
Expand Down
39 changes: 23 additions & 16 deletions src/JellyBox/ViewModels/HomeViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,34 @@ public HomeViewModel(JellyfinApiClient jellyfinApiClient, NavigationManager navi
_navigationManager = navigationManager;
}

public async Task InitializeAsync()
public async void Initialize()
{
Task<Section?>[] sectionTasks =
[
GetUserViewsSectionAsync(),
GetResumeSectionAsync("Continue Watching", MediaType.Video),
GetResumeSectionAsync("Continue Listening", MediaType.Audio),
GetResumeSectionAsync("Continue Reading", MediaType.Book),
// TODO: LiveTV Section,
GetNextUpSectionAsync(),
// TODO: LatestMedia Sections
];

Section?[] sections = await Task.WhenAll(sectionTasks);
foreach (Section? section in sections)
try
{
if (section is not null)
Task<Section?>[] sectionTasks =
[
GetUserViewsSectionAsync(),
GetResumeSectionAsync("Continue Watching", MediaType.Video),
GetResumeSectionAsync("Continue Listening", MediaType.Audio),
GetResumeSectionAsync("Continue Reading", MediaType.Book),
// TODO: LiveTV Section,
GetNextUpSectionAsync(),
// TODO: LatestMedia Sections
];

Section?[] sections = await Task.WhenAll(sectionTasks);
foreach (Section? section in sections)
{
Sections.Add(section);
if (section is not null)
{
Sections.Add(section);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in HomeViewModel.Initialize: {ex}");
}
}

private async Task<Section?> GetUserViewsSectionAsync()
Expand Down
208 changes: 118 additions & 90 deletions src/JellyBox/ViewModels/ItemDetailsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,80 +100,87 @@ public ItemDetailsViewModel(JellyfinApiClient jellyfinApiClient, NavigationManag

internal async void HandleParameters(ItemDetails.Parameters parameters)
{
Item = await _jellyfinApiClient.Items[parameters.ItemId].GetAsync();
try
{
Item = await _jellyfinApiClient.Items[parameters.ItemId].GetAsync();

Name = Item!.Name;
BackdropImageUri = _jellyfinApiClient.GetItemBackdropImageUrl(Item, 1920);
Name = Item!.Name;
BackdropImageUri = _jellyfinApiClient.GetItemBackdropImageUrl(Item, 1920);

List<MediaInfoItem> mediaInfo = new();
if (Item.ProductionYear.HasValue)
{
mediaInfo.Add(new MediaInfoItem(Item.ProductionYear.Value.ToString()));
}
List<MediaInfoItem> mediaInfo = new();
if (Item.ProductionYear.HasValue)
{
mediaInfo.Add(new MediaInfoItem(Item.ProductionYear.Value.ToString()));
}

if (Item.RunTimeTicks.HasValue)
{
mediaInfo.Add(new MediaInfoItem(GetDisplayDuration(Item.RunTimeTicks.Value)));
}
if (Item.RunTimeTicks.HasValue)
{
mediaInfo.Add(new MediaInfoItem(GetDisplayDuration(Item.RunTimeTicks.Value)));
}

if (!string.IsNullOrEmpty(Item.OfficialRating))
{
// TODO: Style correctly
mediaInfo.Add(new MediaInfoItem(Item.OfficialRating));
}
if (!string.IsNullOrEmpty(Item.OfficialRating))
{
// TODO: Style correctly
mediaInfo.Add(new MediaInfoItem(Item.OfficialRating));
}

if (Item.CommunityRating.HasValue)
{
// TODO: Style correctly
mediaInfo.Add(new MediaInfoItem(Item.CommunityRating.Value.ToString("F1")));
}
if (Item.CommunityRating.HasValue)
{
// TODO: Style correctly
mediaInfo.Add(new MediaInfoItem(Item.CommunityRating.Value.ToString("F1")));
}

if (Item.CriticRating.HasValue)
{
// TODO: Style correctly
mediaInfo.Add(new MediaInfoItem(Item.CriticRating.Value.ToString()));
}
if (Item.CriticRating.HasValue)
{
// TODO: Style correctly
mediaInfo.Add(new MediaInfoItem(Item.CriticRating.Value.ToString()));
}

if (Item.RunTimeTicks.HasValue)
{
mediaInfo.Add(new MediaInfoItem(GetEndsAt(Item.RunTimeTicks.Value)));
}
if (Item.RunTimeTicks.HasValue)
{
mediaInfo.Add(new MediaInfoItem(GetEndsAt(Item.RunTimeTicks.Value)));
}

MediaInfo = new ObservableCollection<MediaInfoItem>(mediaInfo);
MediaInfo = new ObservableCollection<MediaInfoItem>(mediaInfo);

if (Item.MediaSources is not null && Item.MediaSources.Count > 0)
{
SourceContainers = new ObservableCollection<MediaSourceInfoWrapper>(Item.MediaSources.Select(s => new MediaSourceInfoWrapper(s.Name!, s)));
if (Item.MediaSources is not null && Item.MediaSources.Count > 0)
{
SourceContainers = new ObservableCollection<MediaSourceInfoWrapper>(Item.MediaSources.Select(s => new MediaSourceInfoWrapper(s.Name!, s)));

// This will trigger OnSelectedSourceContainerChanged, which populates the video, audio, and subtitle drop-downs.
SelectedSourceContainer = SourceContainers[0];
}
// This will trigger OnSelectedSourceContainerChanged, which populates the video, audio, and subtitle drop-downs.
SelectedSourceContainer = SourceContainers[0];
}

TagLine = Item.Taglines is not null && Item.Taglines.Count > 0 ? Item.Taglines[0] : null;
Overview = Item.Overview;
Tags = Item.Tags is not null ? $"Tags: {string.Join(", ", Item.Tags)}" : null;
TagLine = Item.Taglines is not null && Item.Taglines.Count > 0 ? Item.Taglines[0] : null;
Overview = Item.Overview;
Tags = Item.Tags is not null ? $"Tags: {string.Join(", ", Item.Tags)}" : null;

UpdateUserData();
UpdateUserData();

Task<Section?>[] sectionTasks =
[
GetNextUpSectionAsync(),
GetChildrenSectionAsync(),
// TODO: Cast & Crew -->
// TODO: More Like This -->
];
Task<Section?>[] sectionTasks =
[
GetNextUpSectionAsync(),
GetChildrenSectionAsync(),
// TODO: Cast & Crew -->
// TODO: More Like This -->
];

List<Section> sections = new(sectionTasks.Length);
foreach (Task<Section?> sectionTask in sectionTasks)
{
Section? section = await sectionTask;
if (section is not null)
List<Section> sections = new(sectionTasks.Length);
foreach (Task<Section?> sectionTask in sectionTasks)
{
sections.Add(section);
Section? section = await sectionTask;
if (section is not null)
{
sections.Add(section);
}
}
}

Sections = sections;
Sections = sections;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in HandleParameters: {ex}");
}
}

partial void OnSelectedSourceContainerChanged(MediaSourceInfoWrapper? value)
Expand Down Expand Up @@ -296,60 +303,81 @@ public void Play()

public async void PlayTrailer()
{
if (Item is null)
try
{
return;
}
if (Item is null)
{
return;
}

if (Item.LocalTrailerCount > 0)
{
List<BaseItemDto>? localTrailers = await _jellyfinApiClient.Items[Item.Id!.Value].LocalTrailers.GetAsync();
if (localTrailers is not null && localTrailers.Count > 0)
if (Item.LocalTrailerCount > 0)
{
List<BaseItemDto>? localTrailers = await _jellyfinApiClient.Items[Item.Id!.Value].LocalTrailers.GetAsync();
if (localTrailers is not null && localTrailers.Count > 0)
{
// TODO play all the trailers instead of just the first?
_navigationManager.NavigateToVideo(
localTrailers[0],
mediaSourceId: null,
audioStreamIndex: null,
subtitleStreamIndex: null);
return;
}
}

if (Item.RemoteTrailers is not null && Item.RemoteTrailers.Count > 0)
{
// TODO play all the trailers instead of just the first?
_navigationManager.NavigateToVideo(
localTrailers[0],
mediaSourceId: null,
audioStreamIndex: null,
subtitleStreamIndex: null);
Uri videoUri = GetWebVideoUri(Item.RemoteTrailers[0].Url!);

_navigationManager.NavigateToWebVideo(videoUri);
return;
}
}

if (Item.RemoteTrailers is not null && Item.RemoteTrailers.Count > 0)
catch (Exception ex)
{
// TODO play all the trailers instead of just the first?
Uri videoUri = GetWebVideoUri(Item.RemoteTrailers[0].Url!);

_navigationManager.NavigateToWebVideo(videoUri);
return;
System.Diagnostics.Debug.WriteLine($"Error in PlayTrailer: {ex}");
}
}

public async void TogglePlayed()
{
if (Item is null)
try
{
return;
}
if (Item is null)
{
return;
}

Item.UserData = Item.UserData!.Played.GetValueOrDefault()
? await _jellyfinApiClient.UserPlayedItems[Item.Id!.Value].DeleteAsync()
: await _jellyfinApiClient.UserPlayedItems[Item.Id!.Value].PostAsync();
UpdateUserData();
Item.UserData = Item.UserData!.Played.GetValueOrDefault()
? await _jellyfinApiClient.UserPlayedItems[Item.Id!.Value].DeleteAsync()
: await _jellyfinApiClient.UserPlayedItems[Item.Id!.Value].PostAsync();
UpdateUserData();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in TogglePlayed: {ex}");
}
}

public async void ToggleFavorite()
{
if (Item is null)
try
{
return;
}
if (Item is null)
{
return;
}

Item.UserData = Item.UserData!.IsFavorite.GetValueOrDefault()
? await _jellyfinApiClient.UserFavoriteItems[Item.Id!.Value].DeleteAsync()
: await _jellyfinApiClient.UserFavoriteItems[Item.Id!.Value].PostAsync();
UpdateUserData();
Item.UserData = Item.UserData!.IsFavorite.GetValueOrDefault()
? await _jellyfinApiClient.UserFavoriteItems[Item.Id!.Value].DeleteAsync()
: await _jellyfinApiClient.UserFavoriteItems[Item.Id!.Value].PostAsync();
UpdateUserData();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in ToggleFavorite: {ex}");
}
}

// Return a string in '{}h {}m' format for duration.
Expand Down
Loading