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 @@
namespace ErsatzTV.Core.Health; 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
{ {
public abstract string Title { get; } public abstract string Title { get; }
protected HealthCheckResult Result(HealthCheckStatus status, string message) => protected HealthCheckResult Result(HealthCheckStatus status, string message, string briefMessage) =>
new(Title, status, message, None); new(Title, status, message, briefMessage, None);
protected HealthCheckResult NotApplicableResult() => protected HealthCheckResult NotApplicableResult() =>
new(Title, HealthCheckStatus.NotApplicable, string.Empty, None); new(Title, HealthCheckStatus.NotApplicable, string.Empty, string.Empty, None);
protected HealthCheckResult OkResult() => protected HealthCheckResult OkResult() =>
new(Title, HealthCheckStatus.Pass, string.Empty, None); new(Title, HealthCheckStatus.Pass, string.Empty, string.Empty, None);
protected HealthCheckResult FailResult(string message) => protected HealthCheckResult FailResult(string message, string briefMessage) =>
new(Title, HealthCheckStatus.Fail, message, None); new(Title, HealthCheckStatus.Fail, message, briefMessage, None);
protected HealthCheckResult WarningResult(string message) => protected HealthCheckResult WarningResult(string message, string briefMessage) =>
new(Title, HealthCheckStatus.Warning, message, None); new(Title, HealthCheckStatus.Warning, message, briefMessage, None);
protected HealthCheckResult WarningResult(string message, string link) => protected HealthCheckResult WarningResult(string message, string briefMessage, string link) =>
new(Title, HealthCheckStatus.Warning, message, link); new(Title, HealthCheckStatus.Warning, message, briefMessage, link);
protected HealthCheckResult InfoResult(string message) => protected HealthCheckResult InfoResult(string message, string briefMessage) =>
new(Title, HealthCheckStatus.Info, message, None); new(Title, HealthCheckStatus.Info, message, briefMessage, None);
protected static async Task<string> GetProcessOutput( protected static async Task<string> GetProcessOutput(
string path, string path,

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

@ -38,7 +38,8 @@ public class EpisodeMetadataHealthCheck : BaseHealthCheck, IEpisodeMetadataHealt
var folders = string.Join(", ", paths); var folders = string.Join(", ", paths);
return WarningResult( 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(); return OkResult();

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

@ -20,11 +20,12 @@ public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck
{ {
return Result( return Result(
HealthCheckStatus.Pass, 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(); .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(); .AsTask();
} }
} }

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

@ -25,7 +25,8 @@ public class FFmpegReportsHealthCheck : BaseHealthCheck, IFFmpegReportsHealthChe
{ {
return Result( return Result(
HealthCheckStatus.Warning, 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
await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath); await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath);
if (maybeFFmpegPath.IsNone) if (maybeFFmpegPath.IsNone)
{ {
return FailResult("Unable to locate ffmpeg"); return FailResult("Unable to locate ffmpeg", "Unable to locate ffmpeg");
} }
Option<ConfigElement> maybeFFprobePath = Option<ConfigElement> maybeFFprobePath =
await _configElementRepository.GetConfigElement(ConfigElementKey.FFprobePath); await _configElementRepository.GetConfigElement(ConfigElementKey.FFprobePath);
if (maybeFFprobePath.IsNone) if (maybeFFprobePath.IsNone)
{ {
return FailResult("Unable to locate ffprobe"); return FailResult("Unable to locate ffprobe", "Unable to locate ffprobe");
} }
foreach (ConfigElement ffmpegPath in maybeFFmpegPath) foreach (ConfigElement ffmpegPath in maybeFFmpegPath)
@ -42,7 +42,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
Option<string> maybeVersion = await GetVersion(ffmpegPath.Value, cancellationToken); Option<string> maybeVersion = await GetVersion(ffmpegPath.Value, cancellationToken);
if (maybeVersion.IsNone) 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) foreach (string version in maybeVersion)
@ -59,7 +59,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
Option<string> maybeVersion = await GetVersion(ffprobePath.Value, cancellationToken); Option<string> maybeVersion = await GetVersion(ffprobePath.Value, cancellationToken);
if (maybeVersion.IsNone) 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) foreach (string version in maybeVersion)
@ -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) private Option<HealthCheckResult> ValidateVersion(string version, string app)
@ -81,7 +81,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
version.StartsWith("5.", StringComparison.OrdinalIgnoreCase) || version.StartsWith("5.", StringComparison.OrdinalIgnoreCase) ||
version.StartsWith("6.", 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) && if (!version.StartsWith("7.1.1", StringComparison.OrdinalIgnoreCase) &&
@ -90,7 +90,7 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe
version != BundledVersionVaapi) version != BundledVersionVaapi)
{ {
return WarningResult( 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; return None;

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

@ -38,7 +38,7 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath); await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath);
if (maybeFFmpegPath.IsNone) 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>() string version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
@ -67,7 +67,9 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
if (accelerationKinds.Count == 0) 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); Option<HealthCheckResult> maybeResult = await VerifyProfilesUseAcceleration(accelerationKinds);
@ -94,7 +96,8 @@ public class HardwareAccelerationHealthCheck : BaseHealthCheck, IHardwareAcceler
var accel = string.Join(", ", accelerationKinds); var accel = string.Join(", ", accelerationKinds);
var channels = string.Join(", ", badChannels.Map(c => $"{c.Number} - {c.Name}")); var channels = string.Join(", ", badChannels.Map(c => $"{c.Number} - {c.Name}"));
return WarningResult( 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; return None;

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

@ -21,7 +21,7 @@ public class MacOsConfigFolderHealthCheck : BaseHealthCheck, IMacOsConfigFolderH
{ {
var message = 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"; $"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(); return NotApplicableResult().AsTask();

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

@ -38,7 +38,8 @@ public class MovieMetadataHealthCheck : BaseHealthCheck, IMovieMetadataHealthChe
var folders = string.Join(", ", paths); var folders = string.Join(", ", paths);
return WarningResult( 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(); return OkResult();

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

@ -17,7 +17,8 @@ public class UnifiedDockerHealthCheck : BaseHealthCheck, IUnifiedDockerHealthChe
if (InfoVersion.Contains("docker-vaapi") || InfoVersion.Contains("docker-nvidia")) if (InfoVersion.Contains("docker-vaapi") || InfoVersion.Contains("docker-nvidia"))
{ {
return WarningResult( 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(); .AsTask();
} }

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

@ -71,7 +71,8 @@ public class VaapiDriverHealthCheck(
if (capabilities is VaapiHardwareCapabilities { EntrypointCount: 0 } or NoHardwareCapabilities) if (capabilities is VaapiHardwareCapabilities { EntrypointCount: 0 } or NoHardwareCapabilities)
{ {
return FailResult( 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(
return defaultProfiles.Count != 0 return defaultProfiles.Count != 0
? InfoResult( ? 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(); : OkResult();
} }

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

@ -63,7 +63,9 @@ public class ZeroDurationHealthCheck : BaseHealthCheck, IZeroDurationHealthCheck
var files = string.Join(", ", paths); 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(); return OkResult();

1
ErsatzTV.Infrastructure/Health/HealthCheckService.cs

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

29
ErsatzTV/Pages/Index.razor

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

241
ErsatzTV/Pages/Libraries.razor

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

723
ErsatzTV/Pages/Search.razor

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

Loading…
Cancel
Save