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
4 changes: 4 additions & 0 deletions src/JellyBox/Models/Card.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ internal sealed record Card : INavigable
{
public required string Name { get; init; }

public string? SecondaryText { get; init; }

public required int ImageWidth { get; init; }

public required int ImageHeight { get; init; }
Expand All @@ -41,4 +43,6 @@ internal sealed record Card : INavigable
public string UnplayedCountText => UnplayedItemCount >= 100 ? "99+" : UnplayedItemCount.ToString(CultureInfo.InvariantCulture);

public double ProgressWidth => ImageWidth * PlayedPercentage / 100.0;

public bool HasSecondaryText => !string.IsNullOrEmpty(SecondaryText);
}
60 changes: 58 additions & 2 deletions src/JellyBox/Models/CardFactory.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using CommunityToolkit.Mvvm.Input;
using JellyBox.Services;
using Jellyfin.Sdk.Generated.Models;
Expand Down Expand Up @@ -62,7 +63,8 @@ public Card CreateFromItem(

return new Card
{
Name = item.Name!,
Name = GetDisplayName(item),
SecondaryText = GetSecondaryText(item),
ImageWidth = imageWidth,
ImageHeight = imageHeight,
Image = image,
Expand All @@ -86,14 +88,68 @@ public Card CreateFromPerson(
return new Card
{
Name = person.Name!,
// TODO: Cards need secondardy text to display Role
SecondaryText = person.Role,
ImageWidth = imageWidth,
ImageHeight = imageHeight,
Image = image,
NavigateCommand = new RelayCommand(() => _navigationManager.NavigateToPerson(person)),
};
}

private static string GetDisplayName(BaseItemDto item)
=> item.Type switch
{
BaseItemDto_Type.Episode => item.SeriesName ?? item.Name!,
_ => item.Name!,
};

private static string? GetSecondaryText(BaseItemDto item)
=> item.Type switch
{
BaseItemDto_Type.Episode => GetEpisodeText(item),
BaseItemDto_Type.Season => item.SeriesName,
BaseItemDto_Type.MusicAlbum => item.AlbumArtist,
BaseItemDto_Type.Audio => item.AlbumArtist ?? item.Album,
BaseItemDto_Type.MusicVideo => item.AlbumArtist ?? item.Album,
BaseItemDto_Type.Series => GetSeriesYearText(item),
BaseItemDto_Type.Movie => item.ProductionYear?.ToString(CultureInfo.InvariantCulture),
_ => null,
};

private static string? GetSeriesYearText(BaseItemDto item)
{
if (item.ProductionYear is null)
{
return null;
}

string startYear = item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture);

if (string.Equals(item.Status, "Continuing", StringComparison.OrdinalIgnoreCase))
{
return $"{startYear} - Present";
}

if (item.EndDate.HasValue)
{
string endYear = item.EndDate.Value.Year.ToString(CultureInfo.InvariantCulture);
return endYear == startYear ? startYear : $"{startYear} - {endYear}";
}

return startYear;
}

private static string GetEpisodeText(BaseItemDto item)
{
string prefix = item.ParentIndexNumber.HasValue && item.IndexNumber.HasValue
? $"S{item.ParentIndexNumber.Value}:E{item.IndexNumber.Value} - "
: item.IndexNumber.HasValue
? $"E{item.IndexNumber.Value} - "
: string.Empty;

return $"{prefix}{item.Name}";
}

private static double GetAspectRatio(CardShape shape)
=> shape switch
{
Expand Down
15 changes: 14 additions & 1 deletion src/JellyBox/Resources/Templates.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
CornerRadius="{StaticResource CardCornerRadius}"
Expand Down Expand Up @@ -142,7 +143,19 @@
MaxLines="2"
Foreground="{StaticResource TextSecondary}"
FontSize="{StaticResource FontS}"
Padding="4,10,4,10"
Padding="4,10,4,0"
MaxWidth="{x:Bind ImageWidth}" />
<TextBlock
Grid.Row="2"
Visibility="{x:Bind HasSecondaryText}"
Text="{x:Bind SecondaryText}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
MaxLines="1"
Foreground="{StaticResource TextSecondary}"
FontSize="{StaticResource FontXS}"
Opacity="0.7"
Padding="4,2,4,0"
MaxWidth="{x:Bind ImageWidth}" />
</Grid>
</DataTemplate>
Expand Down
46 changes: 43 additions & 3 deletions src/JellyBox/ViewModels/ItemDetailsViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
Expand Down Expand Up @@ -270,9 +271,7 @@ private void DetermineVideoOptions(MediaSourceInfo mediaSourceInfo)
string? displayTitle = videoStream.DisplayTitle;
if (string.IsNullOrEmpty(displayTitle))
{
// DisplayTitle isn't always populated for video
// TODO: Get the resolution text and codec. See /src/controllers/itemDetails/index.js::renderVideoSelections
displayTitle = "TODO";
displayTitle = GetVideoStreamDisplayTitle(videoStream);
}

int index = videoStream.Index.GetValueOrDefault();
Expand Down Expand Up @@ -708,6 +707,47 @@ private static Uri GetWebVideoUri(string url)
@"(?<urlBase>https://www.youtube.com)/watch\?v=(?<id>[^&]+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture);

private static string GetVideoStreamDisplayTitle(MediaStream videoStream)
{
List<string> parts = [];

string? resolutionText = GetResolutionText(videoStream);
if (resolutionText is not null)
{
parts.Add(resolutionText);
}

if (!string.IsNullOrEmpty(videoStream.Codec))
{
parts.Add(videoStream.Codec.ToUpperInvariant());
}

return parts.Count > 0 ? string.Join(' ', parts) : string.Create(CultureInfo.InvariantCulture, $"{videoStream.Width}x{videoStream.Height}");
}

private static string? GetResolutionText(MediaStream stream)
{
int? width = stream.Width;
int? height = stream.Height;

if (width is null || height is null)
{
return null;
}

bool isInterlaced = stream.IsInterlaced ?? false;

return (width, height) switch
{
( >= 3800, _) or (_, >= 2000) => "4K",
( >= 2500, _) or (_, >= 1400) => isInterlaced ? "1440i" : "1440p",
( >= 1800, _) or (_, >= 1000) => isInterlaced ? "1080i" : "1080p",
( >= 1200, _) or (_, >= 700) => isInterlaced ? "720i" : "720p",
( >= 700, _) or (_, >= 400) => isInterlaced ? "480i" : "480p",
_ => null,
};
}

private sealed class MediaStreamComparer : IComparer<MediaStream>
{
private MediaStreamComparer()
Expand Down
24 changes: 16 additions & 8 deletions src/JellyBox/ViewModels/LoginViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using JellyBox.Services;
Expand Down Expand Up @@ -68,7 +69,7 @@ public async void Initialize()
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in LoginViewModel.Initialize: {ex}");
Debug.WriteLine($"Error in LoginViewModel.Initialize: {ex}");
}
}

Expand Down Expand Up @@ -105,8 +106,8 @@ private async Task SignInAsync(CancellationToken cancellationToken)
}
catch (Exception ex)
{
// TODO: Need a friendlier message.
UpdateErrorMessage(ex.Message);
Debug.WriteLine($"Login failed: {ex}");
UpdateErrorMessage(GetFriendlyErrorMessage(ex));
return;
}
finally
Expand Down Expand Up @@ -177,16 +178,16 @@ await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(
{
// Timer callbacks with async void can crash the app if exceptions propagate.
// Log and suppress to prevent app termination.
System.Diagnostics.Debug.WriteLine($"Error in PollQuickConnectAsync: {ex}");
Debug.WriteLine($"Error in PollQuickConnectAsync: {ex}");
}
}

_ = await quickConnectDialog.ShowAsync();
}
catch (Exception ex)
{
// TODO: Need a friendlier message.
UpdateErrorMessage(ex.Message);
Debug.WriteLine($"Quick Connect failed: {ex}");
UpdateErrorMessage(GetFriendlyErrorMessage(ex));
return;
}
finally
Expand All @@ -207,8 +208,7 @@ private bool HandleAuthenticationResult(AuthenticationResult? authenticationResu
string? accessToken = authenticationResult?.AccessToken;
if (accessToken is null)
{
// TODO
UpdateErrorMessage("Unexpected authentication failure");
UpdateErrorMessage("Authentication failed. Please try again.");
return false;
}

Expand All @@ -232,4 +232,12 @@ private void UpdateErrorMessage(string message)
ShowErrorMessage = true;
ErrorMessage = message;
}

private static string GetFriendlyErrorMessage(Exception ex)
=> ex switch
{
HttpRequestException => "Unable to connect to the server. Please check your connection and try again.",
TaskCanceledException => "The request timed out. Please try again.",
_ => "An error occurred during login. Please try again.",
};
}