Browse Source

improve search results ui on mobile (#2133)

* show brief health check messages on mobile

* update libraries layout

* improve search results ui on mobile
pull/2134/head
Jason Dove 1 year ago committed by GitHub
parent
commit
7e40a809ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      ErsatzTV.Core/Health/HealthCheckResult.cs
  2. 24
      ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs
  3. 3
      ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs
  4. 5
      ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs
  5. 3
      ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs
  6. 14
      ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs
  7. 9
      ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs
  8. 2
      ErsatzTV.Infrastructure/Health/Checks/MacOsConfigFolderHealthCheck.cs
  9. 3
      ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs
  10. 3
      ErsatzTV.Infrastructure/Health/Checks/UnifiedDockerHealthCheck.cs
  11. 6
      ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs
  12. 4
      ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs
  13. 1
      ErsatzTV.Infrastructure/Health/HealthCheckService.cs
  14. 29
      ErsatzTV/Pages/Index.razor
  15. 241
      ErsatzTV/Pages/Libraries.razor
  16. 723
      ErsatzTV/Pages/Search.razor

2
ErsatzTV.Core/Health/HealthCheckResult.cs

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
namespace ErsatzTV.Core.Health;
public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message, Option<string> Link);
public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message, string BriefMessage, Option<string> Link);

24
ErsatzTV.Infrastructure/Health/Checks/BaseHealthCheck.cs

@ -9,26 +9,26 @@ public abstract class BaseHealthCheck @@ -9,26 +9,26 @@ public abstract class BaseHealthCheck
{
public abstract string Title { get; }
protected HealthCheckResult Result(HealthCheckStatus status, string message) =>
new(Title, status, message, None);
protected HealthCheckResult Result(HealthCheckStatus status, string message, string briefMessage) =>
new(Title, status, message, briefMessage, None);
protected HealthCheckResult NotApplicableResult() =>
new(Title, HealthCheckStatus.NotApplicable, string.Empty, None);
new(Title, HealthCheckStatus.NotApplicable, string.Empty, string.Empty, None);
protected HealthCheckResult OkResult() =>
new(Title, HealthCheckStatus.Pass, string.Empty, None);
new(Title, HealthCheckStatus.Pass, string.Empty, string.Empty, None);
protected HealthCheckResult FailResult(string message) =>
new(Title, HealthCheckStatus.Fail, message, None);
protected HealthCheckResult FailResult(string message, string briefMessage) =>
new(Title, HealthCheckStatus.Fail, message, briefMessage, None);
protected HealthCheckResult WarningResult(string message) =>
new(Title, HealthCheckStatus.Warning, message, None);
protected HealthCheckResult WarningResult(string message, string briefMessage) =>
new(Title, HealthCheckStatus.Warning, message, briefMessage, None);
protected HealthCheckResult WarningResult(string message, string link) =>
new(Title, HealthCheckStatus.Warning, message, link);
protected HealthCheckResult WarningResult(string message, string briefMessage, string link) =>
new(Title, HealthCheckStatus.Warning, message, briefMessage, link);
protected HealthCheckResult InfoResult(string message) =>
new(Title, HealthCheckStatus.Info, message, None);
protected HealthCheckResult InfoResult(string message, string briefMessage) =>
new(Title, HealthCheckStatus.Info, message, briefMessage, None);
protected static async Task<string> GetProcessOutput(
string path,

3
ErsatzTV.Infrastructure/Health/Checks/EpisodeMetadataHealthCheck.cs

@ -38,7 +38,8 @@ public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealt @@ -38,7 +38,8 @@ public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealt
var folders = string.Join(", ", paths);
return WarningResult(
$"There are {episodes.Count} episodes with missing metadata, including in the following folders: {folders}");
$"There are {episodes.Count} episodes with missing metadata, including in the following folders: {folders}",
$"There are {episodes.Count} episodes with missing metadata");
}
return OkResult();

5
ErsatzTV.Infrastructure/Health/Checks/ErrorReportsHealthCheck.cs

@ -20,11 +20,12 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck @@ -20,11 +20,12 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck
{
return Result(
HealthCheckStatus.Pass,
"Automated error reporting is enabled, thank you! To disable, edit the file appsettings.json or set the Bugsnag:Enable environment variable to false")
"Automated error reporting is enabled, thank you! To disable, edit the file appsettings.json or set the Bugsnag:Enable environment variable to false",
"Automated error reporting is enabled, thank you!")
.AsTask();
}
return InfoResult("Automated error reporting is disabled. Please enable to support bug fixing efforts!")
return InfoResult("Automated error reporting is disabled. Please enable to support bug fixing efforts!", "Automated error reporting is disabled")
.AsTask();
}
}

3
ErsatzTV.Infrastructure/Health/Checks/FFmpegReportsHealthCheck.cs

@ -25,7 +25,8 @@ public class FFmpegReportsHealthCheck : BaseHealthCheck, IFFmpegReportsHealthChe @@ -25,7 +25,8 @@ public class FFmpegReportsHealthCheck : BaseHealthCheck, IFFmpegReportsHealthChe
{
return Result(
HealthCheckStatus.Warning,
"FFmpeg troubleshooting reports are enabled and may use a lot of disk space");
"FFmpeg troubleshooting reports are enabled and may use a lot of disk space",
"FFmpeg troubleshooting reports are enabled");
}
}

14
ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs

@ -27,14 +27,14 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -27,14 +27,14 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath);
if (maybeFFmpegPath.IsNone)
{
return FailResult("Unable to locate ffmpeg");
return FailResult("Unable to locate ffmpeg", "Unable to locate ffmpeg");
}
Option<ConfigElement> maybeFFprobePath =
await _configElementRepository.GetConfigElement(ConfigElementKey.FFprobePath);
if (maybeFFprobePath.IsNone)
{
return FailResult("Unable to locate ffprobe");
return FailResult("Unable to locate ffprobe", "Unable to locate ffprobe");
}
foreach (ConfigElement ffmpegPath in maybeFFmpegPath)
@ -42,7 +42,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -42,7 +42,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
Option<string> maybeVersion = await GetVersion(ffmpegPath.Value, cancellationToken);
if (maybeVersion.IsNone)
{
return WarningResult("Unable to determine ffmpeg version");
return WarningResult("Unable to determine ffmpeg version", "Unable to determine ffmpeg version");
}
foreach (string version in maybeVersion)
@ -59,7 +59,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -59,7 +59,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
Option<string> maybeVersion = await GetVersion(ffprobePath.Value, cancellationToken);
if (maybeVersion.IsNone)
{
return WarningResult("Unable to determine ffprobe version");
return WarningResult("Unable to determine ffprobe version", "Unable to determine ffprobe version");
}
foreach (string version in maybeVersion)
@ -71,7 +71,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -71,7 +71,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
}
}
return new HealthCheckResult("FFmpeg Version", HealthCheckStatus.Pass, string.Empty, None);
return new HealthCheckResult("FFmpeg Version", HealthCheckStatus.Pass, string.Empty, string.Empty, None);
}
private Option<HealthCheckResult> ValidateVersion(string version, string app)
@ -81,7 +81,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -81,7 +81,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
version.StartsWith("5.", StringComparison.OrdinalIgnoreCase) ||
version.StartsWith("6.", StringComparison.OrdinalIgnoreCase))
{
return FailResult($"{app} version {version} is too old; please install 7.1.1!");
return FailResult($"{app} version {version} is too old; please install 7.1.1!", $"{app} version is too old");
}
if (!version.StartsWith("7.1.1", StringComparison.OrdinalIgnoreCase) &&
@ -90,7 +90,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -90,7 +90,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
version != BundledVersionVaapi)
{
return WarningResult(
$"{app} version {version} is unexpected and may have problems; please install 7.1.1!");
$"{app} version {version} is unexpected and may have problems; please install 7.1.1!", $"{app} version is unexpected");
}
return None;

9
ErsatzTV.Infrastructure/Health/Checks/HardwareAccelerationHealthCheck.cs

@ -38,7 +38,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler @@ -38,7 +38,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath);
if (maybeFFmpegPath.IsNone)
{
return FailResult("Unable to locate ffmpeg");
return FailResult("Unable to locate ffmpeg", "Unable to locate ffmpeg");
}
string version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
@ -67,7 +67,9 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler @@ -67,7 +67,9 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
if (accelerationKinds.Count == 0)
{
return InfoResult("No compatible hardware acceleration kinds are supported by ffmpeg");
return InfoResult(
"No compatible hardware acceleration kinds are supported by ffmpeg",
"FFmpeg does not support hardware acceleration");
}
Option<HealthCheckResult> maybeResult = await VerifyProfilesUseAcceleration(accelerationKinds);
@ -94,7 +96,8 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler @@ -94,7 +96,8 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
var accel = string.Join(", ", accelerationKinds);
var channels = string.Join(", ", badChannels.Map(c => $"{c.Number} - {c.Name}"));
return WarningResult(
$"The following channels use ffmpeg profiles that are not configured for hardware acceleration ({accel}): {channels}");
$"The following channels use ffmpeg profiles that are not configured for hardware acceleration ({accel}): {channels}",
$"{channels.Length} channels are not configured for hardware acceleration");
}
return None;

2
ErsatzTV.Infrastructure/Health/Checks/MacOsConfigFolderHealthCheck.cs

@ -21,7 +21,7 @@ public class MacOsConfigFolderHealthCheck : BaseHealthCheck, IMacOsConfigFolderH @@ -21,7 +21,7 @@ public class MacOsConfigFolderHealthCheck : BaseHealthCheck, IMacOsConfigFolderH
{
var message =
$"Old config data exists; to migrate: exit ETV, backup the folder {FileSystemLayout.AppDataFolder} to another location, and restart ETV. Otherwise, move the old folder {FileSystemLayout.MacOsOldAppDataFolder} to another location to remove this message";
return FailResult(message).AsTask();
return FailResult(message, "Old config data exists").AsTask();
}
return NotApplicableResult().AsTask();

3
ErsatzTV.Infrastructure/Health/Checks/MovieMetadataHealthCheck.cs

@ -38,7 +38,8 @@ public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthChe @@ -38,7 +38,8 @@ public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthChe
var folders = string.Join(", ", paths);
return WarningResult(
$"There are {movies.Count} movies with missing metadata, including in the following folders: {folders}");
$"There are {movies.Count} movies with missing metadata, including in the following folders: {folders}",
$"There are {movies.Count} movies with missing metadata");
}
return OkResult();

3
ErsatzTV.Infrastructure/Health/Checks/UnifiedDockerHealthCheck.cs

@ -17,7 +17,8 @@ public class UnifiedDockerHealthCheck : BaseHealthCheck, IUnifiedDockerHealthChe @@ -17,7 +17,8 @@ public class UnifiedDockerHealthCheck : BaseHealthCheck, IUnifiedDockerHealthChe
if (InfoVersion.Contains("docker-vaapi") || InfoVersion.Contains("docker-nvidia"))
{
return WarningResult(
"VAAPI and NVIDIA docker tag suffixes are deprecated; please remove `-vaapi` or `-nvidia` and pull the default image.")
"VAAPI and NVIDIA docker tag suffixes are deprecated; please remove `-vaapi` or `-nvidia` and pull the default image.",
"docker tag is deprecated")
.AsTask();
}

6
ErsatzTV.Infrastructure/Health/Checks/VaapiDriverHealthCheck.cs

@ -71,7 +71,8 @@ public class VaapiDriverHealthCheck( @@ -71,7 +71,8 @@ public class VaapiDriverHealthCheck(
if (capabilities is VaapiHardwareCapabilities { EntrypointCount: 0 } or NoHardwareCapabilities)
{
return FailResult(
$"FFmpeg Profile {profile.Name} is using device and driver combination ({profile.VaapiDevice} and {profile.VaapiDriver}) that reports no capabilities. Hardware Acceleration WILL NOT WORK as configured.");
$"FFmpeg Profile {profile.Name} is using device and driver combination ({profile.VaapiDevice} and {profile.VaapiDriver}) that reports no capabilities. Hardware Acceleration WILL NOT WORK as configured.",
"Hardware acceleration WILL NOT WORK...");
}
}
}
@ -83,7 +84,8 @@ public class VaapiDriverHealthCheck( @@ -83,7 +84,8 @@ public class VaapiDriverHealthCheck(
return defaultProfiles.Count != 0
? InfoResult(
$"{defaultProfiles.Count} FFmpeg Profile{(defaultProfiles.Count > 1 ? "s are" : " is")} set to use Default VAAPI Driver; selecting iHD (Gen 8+) or i965 (up to Gen 9) may offer better performance with Intel iGPU")
$"{defaultProfiles.Count} FFmpeg Profile{(defaultProfiles.Count > 1 ? "s are" : " is")} set to use Default VAAPI Driver; selecting iHD (Gen 8+) or i965 (up to Gen 9) may offer better performance with Intel iGPU",
"Default VAAPI driver is not optimal")
: OkResult();
}

4
ErsatzTV.Infrastructure/Health/Checks/ZeroDurationHealthCheck.cs

@ -63,7 +63,9 @@ public class ZeroDurationHealthCheck : BaseHealthCheck, IZeroDurationHealthCheck @@ -63,7 +63,9 @@ public class ZeroDurationHealthCheck : BaseHealthCheck, IZeroDurationHealthCheck
var files = string.Join(", ", paths);
return WarningResult($"There are {all.Count} files with zero duration, including the following: {files}");
return WarningResult(
$"There are {all.Count} files with zero duration, including the following: {files}",
$"There are {all.Count} files with zero duration");
}
return OkResult();

1
ErsatzTV.Infrastructure/Health/HealthCheckService.cs

@ -60,6 +60,7 @@ public class HealthCheckService : IHealthCheckService @@ -60,6 +60,7 @@ public class HealthCheckService : IHealthCheckService
c.Title,
HealthCheckStatus.Fail,
"Health check failure; see logs",
"Health check failure",
None);
return TryAsync(() => c.Check(cancellationToken)).IfFail(ex => LogAndReturn(ex, failedResult));
})

29
ErsatzTV/Pages/Index.razor

@ -84,21 +84,26 @@ @@ -84,21 +84,26 @@
</div>
</MudTd>
<MudTd>
@if (context.Link.IsSome)
{
foreach (string link in context.Link)
<MudHidden Breakpoint="Breakpoint.Xs">
@if (context.Link.IsSome)
{
foreach (string link in context.Link)
{
<MudLink Href="@link">
@context.Message
</MudLink>
}
}
else
{
<MudLink Href="@link">
<MudText>
@context.Message
</MudLink>
</MudText>
}
}
else
{
<MudText>
@context.Message
</MudText>
}
</MudHidden>
<MudHidden Breakpoint="Breakpoint.Xs" Invert="true">
@context.BriefMessage
</MudHidden>
</MudTd>
</RowTemplate>
</MudTable>

241
ErsatzTV/Pages/Libraries.razor

@ -13,132 +13,135 @@ @@ -13,132 +13,135 @@
@inject ChannelWriter<IScannerBackgroundServiceRequest> ScannerWorkerChannel;
@inject ICourier Courier
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_libraries" Dense="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Libraries</MudText>
</ToolBarContent>
<ColGroup>
<col/>
@if (_showServerNames)
{
<col/>
}
<col/>
<col/>
<col style="width: 180px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Library Kind</MudTh>
@if (_showServerNames)
{
<MudTh>Server Name</MudTh>
}
<MudTh>Library Name</MudTh>
<MudTh>Media Kind</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Library Kind">@context.LibraryKind</MudTd>
@if (_showServerNames)
{
<MudTd DataLabel="Server Name">@context.MediaSourceName</MudTd>
}
<MudTd DataLabel="Library Name">@context.Name</MudTd>
<MudTd DataLabel="Media Kind">@context.MediaKind</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
@if (Locker.IsLibraryLocked(context.Id))
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Libraries</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true" Items="_libraries" Dense="true" Class="mb-5">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
@if (_showServerNames)
{
<col/>
}
<col/>
<col/>
<col style="width: 180px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Library Kind</MudTh>
@if (_showServerNames)
{
<div style="width: 48px">
@if (_progressByLibrary[context.Id] > 0)
{
<MudText Color="Color.Primary">
@($"{_progressByLibrary[context.Id]} %")
</MudText>
}
</div>
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div>
<MudTh>Server Name</MudTh>
}
else
<MudTh>Library Name</MudTh>
<MudTh>Media Kind</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Library Kind">@context.LibraryKind</MudTd>
@if (_showServerNames)
{
if (context is PlexLibraryViewModel or EmbyLibraryViewModel or JellyfinLibraryViewModel)
{
<MudTooltip Text="Deep Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.FindReplace"
Disabled="@Locker.IsLibraryLocked(context.Id)"
OnClick="@(_ => ScanLibrary(context, true))">
</MudIconButton>
</MudTooltip>
}
else
{
<div style="width: 48px"></div>
}
<MudTooltip Text="Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsLibraryLocked(context.Id)"
OnClick="@(_ => ScanLibrary(context))">
</MudIconButton>
</MudTooltip>
<MudTd DataLabel="Server Name">@context.MediaSourceName</MudTd>
}
<MudTooltip Text="Search Library">
<MudIconButton Icon="@Icons.Material.Filled.Search"
Href="@($"search?query=library_id%3a{context.Id}")">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
<MudTd DataLabel="Library Name">@context.Name</MudTd>
<MudTd DataLabel="Media Kind">@context.MediaKind</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
@if (Locker.IsLibraryLocked(context.Id))
{
<div style="width: 48px">
@if (_progressByLibrary[context.Id] > 0)
{
<MudText Color="Color.Primary">
@($"{_progressByLibrary[context.Id]} %")
</MudText>
}
</div>
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div>
}
else
{
if (context is PlexLibraryViewModel or EmbyLibraryViewModel or JellyfinLibraryViewModel)
{
<MudTooltip Text="Deep Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.FindReplace"
Disabled="@Locker.IsLibraryLocked(context.Id)"
OnClick="@(_ => ScanLibrary(context, true))">
</MudIconButton>
</MudTooltip>
}
else
{
<div style="width: 48px"></div>
}
@if (_externalCollections.Any())
{
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_externalCollections" Dense="true">
<ToolBarContent>
<MudText Typo="Typo.h6">External Collections</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col style="width: 180px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Library Kind</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Library Kind">@context.LibraryKind</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<div style="width: 48px"></div>
@if (AreCollectionsLocked(context.LibraryKind))
{
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div>
}
else
{
<div style="width: 48px"></div>
<MudTooltip Text="Scan Collections">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@AreCollectionsLocked(context.LibraryKind)"
OnClick="@(_ => ScanExternalCollections(context))">
<MudTooltip Text="Scan Library">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsLibraryLocked(context.Id)"
OnClick="@(_ => ScanLibrary(context))">
</MudIconButton>
</MudTooltip>
}
<MudTooltip Text="Search Library">
<MudIconButton Icon="@Icons.Material.Filled.Search"
Href="@($"search?query=library_id%3a{context.Id}")">
</MudIconButton>
</MudTooltip>
}
<div style="width: 48px"></div>
</div>
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
}
</div>
</MudTd>
</RowTemplate>
</MudTable>
<MudText Typo="Typo.h5" Class="mb-2">External Collections</MudText>
<MudDivider Class="mb-6"/>
@if (_externalCollections.Any())
{
<MudTable Hover="true" Items="_externalCollections" Dense="true">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col style="width: 180px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Library Kind</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd>@context.LibraryKind</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<div style="width: 48px"></div>
@if (AreCollectionsLocked(context.LibraryKind))
{
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
</div>
}
else
{
<div style="width: 48px"></div>
<MudTooltip Text="Scan Collections">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@AreCollectionsLocked(context.LibraryKind)"
OnClick="@(_ => ScanExternalCollections(context))">
</MudIconButton>
</MudTooltip>
}
<div style="width: 48px"></div>
</div>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();

723
ErsatzTV/Pages/Search.razor

@ -8,372 +8,395 @@ @@ -8,372 +8,395 @@
@using ErsatzTV.Extensions
@implements IDisposable
<MudPaper Square="true" Style="display: flex; height: 64px; width: 100%; z-index: 100;">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@if (IsSelectMode())
{
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
<div style="margin-left: auto">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="@(_ => ClearSelection())">
Clear Selection
</MudButton>
</div>
}
else
{
<div style="align-items: center; display: flex; width: 100%" class="d-none d-md-flex">
<MudText Style="margin-bottom: auto; margin-top: auto">@_query</MudText>
@if (_movies?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#movies")" Style="margin-bottom: auto; margin-top: auto">@_movies.Count Movies</MudLink>
}
@if (_shows?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
}
@if (_seasons?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#seasons")" Style="margin-bottom: auto; margin-top: auto">@_seasons.Count Seasons</MudLink>
}
@if (_episodes?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")" Style="margin-bottom: auto; margin-top: auto">@_episodes.Count Episodes</MudLink>
}
@if (_artists?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink>
}
@if (_musicVideos?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")" Style="margin-bottom: auto; margin-top: auto">@_musicVideos.Count Music Videos</MudLink>
}
@if (_otherVideos?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink>
}
@if (_songs?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink>
}
@if (_images?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")" Style="margin-bottom: auto; margin-top: auto">@_images.Count Images</MudLink>
}
<div class="flex-grow-1 d-none d-md-flex"></div>
<div>
<MudTooltip Text="Add All To Collection">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddAllToCollection">
Add All
</MudButton>
</MudTooltip>
<MudTooltip Text="Add All To Playlist">
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@AddAllToPlaylist">
Add All
</MudButton>
</MudTooltip>
<MudTooltip Text="Save As Smart Collection">
<MudButton Class="ml-3" Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Save"
OnClick="@SaveAsSmartCollection">
Save As
</MudButton>
</MudTooltip>
</div>
</div>
<div style="align-items: center; display: flex; width: 100%" class="d-md-none">
<div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add All To Collection" OnClick="@AddAllToCollection"/>
<MudMenuItem Icon="@Icons.Material.Filled.PlaylistAdd" Label="Add All To Playlist" OnClick="AddAllToPlaylist"/>
<MudMenuItem Icon="@Icons.Material.Filled.Save" Label="Save As Smart Collection" OnClick="SaveAsSmartCollection"/>
</MudMenu>
</div>
}
</div>
</MudPaper>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="margin-top: 96px">
@if (_movies?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Movies
</MudText>
@if (_movies.Count > 50)
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100;">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@if (IsSelectMode())
{
<MudLink Href="@GetMoviesLink()" Class="ml-4">See All >></MudLink>
<div style="align-items: center; display: flex; width: 100%;">
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
</div>
<div style="margin-left: auto" class="d-none d-md-flex">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(_ => AddSelectionToCollection())">
Add To Collection
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@(_ => AddSelectionToPlaylist())">
Add To Playlist
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="@(_ => ClearSelection())">
Clear Selection
</MudButton>
</div>
<div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none">
<div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add To Collection" OnClick="@AddSelectionToCollection"/>
<MudMenuItem Icon="@Icons.Material.Filled.PlaylistAdd" Label="Add To Playlist" OnClick="AddSelectionToPlaylist"/>
<MudMenuItem Icon="@Icons.Material.Filled.Check" Label="Clear Selection" OnClick="ClearSelection"/>
</MudMenu>
</div>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle))
else
{
<MediaCard Data="@card"
Href="@($"media/movies/{card.MovieId}")"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
<div style="align-items: center; display: flex; width: 100%" class="d-none d-md-flex">
<MudText>@_query</MudText>
@if (_movies?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#movies")" Style="margin-bottom: auto; margin-top: auto">@_movies.Count Movies</MudLink>
}
@if (_shows?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Shows
</MudText>
@if (_shows.Count > 50)
{
<MudLink Href="@GetShowsLink()" Class="ml-4">See All >></MudLink>
}
</div>
@if (_shows?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
}
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/shows/{card.TelevisionShowId}")"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_seasons?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#seasons")" Style="margin-bottom: auto; margin-top: auto">@_seasons.Count Seasons</MudLink>
}
@if (_seasons?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Seasons
</MudText>
@if (_seasons.Count > 50)
{
<MudLink Href="@GetSeasonsLink()" Class="ml-4">See All >></MudLink>
}
</div>
@if (_episodes?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")" Style="margin-bottom: auto; margin-top: auto">@_episodes.Count Episodes</MudLink>
}
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/seasons/{card.TelevisionSeasonId}")"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_artists?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink>
}
@if (_episodes?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Episodes
</MudText>
@if (_episodes.Count > 50)
{
<MudLink Href="@GetEpisodesLink()" Class="ml-4">See All >></MudLink>
}
</div>
@if (_musicVideos?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")" Style="margin-bottom: auto; margin-top: auto">@_musicVideos.Count Music Videos</MudLink>
}
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
AddToCollectionClicked="@AddToCollection"
Href="@($"media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_otherVideos?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink>
}
@if (_artists?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists
</MudText>
@if (_artists.Count > 50)
{
<MudLink Href="@GetArtistsLink()" Class="ml-4">See All >></MudLink>
@if (_songs?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink>
}
@if (_images?.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")" Style="margin-bottom: auto; margin-top: auto">@_images.Count Images</MudLink>
}
<div class="flex-grow-1 d-none d-md-flex"></div>
<div>
<MudTooltip Text="Add All To Collection">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddAllToCollection">
Add All
</MudButton>
</MudTooltip>
<MudTooltip Text="Add All To Playlist">
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PlaylistAdd"
OnClick="@AddAllToPlaylist">
Add All
</MudButton>
</MudTooltip>
<MudTooltip Text="Save As Smart Collection">
<MudButton Class="ml-3" Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Save"
OnClick="@SaveAsSmartCollection">
Save As
</MudButton>
</MudTooltip>
</div>
</div>
<div style="align-items: center; display: flex; width: 100%" class="d-md-none">
<div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add All To Collection" OnClick="@AddAllToCollection"/>
<MudMenuItem Icon="@Icons.Material.Filled.PlaylistAdd" Label="Add All To Playlist" OnClick="AddAllToPlaylist"/>
<MudMenuItem Icon="@Icons.Material.Filled.Save" Label="Save As Smart Collection" OnClick="SaveAsSmartCollection"/>
</MudMenu>
</div>
}
</div>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@if (_movies?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Movies
</MudText>
@if (_movies.Count > 50)
{
<MudLink Href="@GetMoviesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/music/artists/{card.ArtistId}")"
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/movies/{card.MovieId}")"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_shows?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Shows
</MudText>
@if (_shows.Count > 50)
{
<MudLink Href="@GetShowsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
@if (_musicVideos?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos
</MudText>
@if (_musicVideos.Count > 50)
{
<MudLink Href="@GetMusicVideosLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/shows/{card.TelevisionShowId}")"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_seasons?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Seasons
</MudText>
@if (_seasons.Count > 50)
{
<MudLink Href="@GetSeasonsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/seasons/{card.TelevisionSeasonId}")"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_episodes?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Episodes
</MudText>
@if (_episodes.Count > 50)
{
<MudLink Href="@GetEpisodesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
@if (_otherVideos?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos
</MudText>
@if (_otherVideos.Count > 50)
{
<MudLink Href="@GetOtherVideosLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
AddToCollectionClicked="@AddToCollection"
Href="@($"media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_artists?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists
</MudText>
@if (_artists.Count > 50)
{
<MudLink Href="@GetArtistsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/music/artists/{card.ArtistId}")"
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_musicVideos?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos
</MudText>
@if (_musicVideos.Count > 50)
{
<MudLink Href="@GetMusicVideosLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
@if (_songs?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Songs
</MudText>
@if (_songs.Count > 50)
{
<MudLink Href="@GetSongsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_otherVideos?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos
</MudText>
@if (_otherVideos.Count > 50)
{
<MudLink Href="@GetOtherVideosLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_songs?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Songs
</MudText>
@if (_songs.Count > 50)
{
<MudLink Href="@GetSongsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
@if (_images?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Images
</MudText>
@if (_images.Count > 50)
{
<MudLink Href="@GetImagesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_images?.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Images
</MudText>
@if (_images.Count > 50)
{
<MudLink Href="@GetImagesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (ImageCardViewModel card in _images.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (ImageCardViewModel card in _images.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
</MudContainer>
}
</MudContainer>
</div>
</MudForm>
@code {
private string _query;
@ -525,7 +548,7 @@ @@ -525,7 +548,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddMovieToCollection(collection.Id, movie.MovieId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
@ -546,7 +569,7 @@ @@ -546,7 +569,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddShowToCollection(collection.Id, show.TelevisionShowId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
@ -567,7 +590,7 @@ @@ -567,7 +590,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
@ -588,7 +611,7 @@ @@ -588,7 +611,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddArtistToCollection(collection.Id, artist.ArtistId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
@ -609,7 +632,7 @@ @@ -609,7 +632,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
@ -630,7 +653,7 @@ @@ -630,7 +653,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddOtherVideoToCollection(collection.Id, otherVideo.OtherVideoId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);
@ -651,7 +674,7 @@ @@ -651,7 +674,7 @@
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
if (result is { Canceled: false, Data: MediaCollectionViewModel collection })
{
var request = new AddSongToCollection(collection.Id, song.SongId);
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken);

Loading…
Cancel
Save