Browse Source

new movie layout, new dark ui (#55)

* include cache header on artwork responses

* rework movie page to include fan art

* full width app bar

* dark mode

* cleanup

* fix placeholder color
pull/1213/head
Jason Dove 5 years ago committed by GitHub
parent
commit
0cdc2d22ef
  1. 1
      ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs
  2. 8
      ErsatzTV.Application/Movies/Mapper.cs
  3. 2
      ErsatzTV.Application/Movies/MovieViewModel.cs
  4. 3
      ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs
  5. 1
      ErsatzTV.Core/FileSystemLayout.cs
  6. 6
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  7. 38
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  8. 2
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  9. 1
      ErsatzTV.sln.DotSettings
  10. 11
      ErsatzTV/Controllers/ArtworkController.cs
  11. 94
      ErsatzTV/Pages/ChannelEditor.razor
  12. 24
      ErsatzTV/Pages/Channels.razor
  13. 34
      ErsatzTV/Pages/CollectionEditor.razor
  14. 128
      ErsatzTV/Pages/CollectionItems.razor
  15. 30
      ErsatzTV/Pages/Collections.razor
  16. 32
      ErsatzTV/Pages/FFmpeg.razor
  17. 185
      ErsatzTV/Pages/FFmpegEditor.razor
  18. 138
      ErsatzTV/Pages/Index.razor
  19. 2
      ErsatzTV/Pages/Libraries.razor
  20. 2
      ErsatzTV/Pages/LocalLibraryEditor.razor
  21. 2
      ErsatzTV/Pages/LocalLibraryPathEditor.razor
  22. 2
      ErsatzTV/Pages/Logs.razor
  23. 57
      ErsatzTV/Pages/Movie.razor
  24. 46
      ErsatzTV/Pages/MovieList.razor
  25. 36
      ErsatzTV/Pages/PlayoutEditor.razor
  26. 2
      ErsatzTV/Pages/Playouts.razor
  27. 46
      ErsatzTV/Pages/ScheduleEditor.razor
  28. 252
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  29. 49
      ErsatzTV/Pages/Schedules.razor
  30. 2
      ErsatzTV/Pages/Search.razor
  31. 54
      ErsatzTV/Pages/TelevisionEpisode.razor
  32. 92
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  33. 84
      ErsatzTV/Pages/TelevisionSeasonList.razor
  34. 46
      ErsatzTV/Pages/TelevisionShowList.razor
  35. 43
      ErsatzTV/Shared/MainLayout.razor
  36. 4
      ErsatzTV/Shared/MediaCard.razor
  37. 38
      ErsatzTV/wwwroot/css/site.css
  38. BIN
      ErsatzTV/wwwroot/images/ersatztv.png

1
ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs

@ -42,6 +42,7 @@ namespace ErsatzTV.Application.Images.Queries
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder), ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder), ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder), ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder _ => FileSystemLayout.LegacyImageCacheFolder
}; };

8
ErsatzTV.Application/Movies/Mapper.cs

@ -13,8 +13,12 @@ namespace ErsatzTV.Application.Movies
metadata.Title, metadata.Title,
metadata.Year?.ToString(), metadata.Year?.ToString(),
metadata.Plot, metadata.Plot,
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster)) Artwork(metadata, ArtworkKind.Poster),
.Match(a => a.Path, string.Empty)); Artwork(metadata, ArtworkKind.FanArt));
} }
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
} }
} }

2
ErsatzTV.Application/Movies/MovieViewModel.cs

@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Movies namespace ErsatzTV.Application.Movies
{ {
public record MovieViewModel(string Title, string Year, string Plot, string Poster); public record MovieViewModel(string Title, string Year, string Plot, string Poster, string FanArt);
} }

3
ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs

@ -4,6 +4,7 @@
{ {
Poster = 0, Poster = 0,
Thumbnail = 1, Thumbnail = 1,
Logo = 2 Logo = 2,
FanArt = 3
} }
} }

1
ErsatzTV.Core/FileSystemLayout.cs

@ -24,5 +24,6 @@ namespace ErsatzTV.Core
public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters"); public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");
public static readonly string ThumbnailCacheFolder = Path.Combine(ArtworkCacheFolder, "thumbnails"); public static readonly string ThumbnailCacheFolder = Path.Combine(ArtworkCacheFolder, "thumbnails");
public static readonly string LogoCacheFolder = Path.Combine(ArtworkCacheFolder, "logos"); public static readonly string LogoCacheFolder = Path.Combine(ArtworkCacheFolder, "logos");
public static readonly string FanArtCacheFolder = Path.Combine(ArtworkCacheFolder, "fanart");
} }
} }

6
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -97,10 +97,10 @@ namespace ErsatzTV.Core.Metadata
metadata.Artwork ??= new List<Artwork>(); metadata.Artwork ??= new List<Artwork>();
Option<Artwork> maybePoster = Option<Artwork> maybeArtwork =
Optional(metadata.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == artworkKind); Optional(metadata.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == artworkKind);
bool shouldRefresh = maybePoster.Match( bool shouldRefresh = maybeArtwork.Match(
artwork => artwork.DateUpdated < lastWriteTime, artwork => artwork.DateUpdated < lastWriteTime,
true); true);
@ -109,7 +109,7 @@ namespace ErsatzTV.Core.Metadata
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile); _logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind); string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
maybePoster.Match( maybeArtwork.Match(
artwork => artwork =>
{ {
artwork.Path = cacheName; artwork.Path = cacheName;

38
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -80,7 +80,8 @@ namespace ErsatzTV.Core.Metadata
.GetOrAdd(libraryPath, file) .GetOrAdd(libraryPath, file)
.BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie)) .BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie))
.BindT(UpdateMetadata) .BindT(UpdateMetadata)
.BindT(UpdatePoster); .BindT(UpdatePoster)
.BindT(UpdateFanArt);
maybeMovie.IfLeft( maybeMovie.IfLeft(
error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value)); error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value));
@ -139,7 +140,7 @@ namespace ErsatzTV.Core.Metadata
{ {
try try
{ {
await LocatePoster(movie).IfSomeAsync( await LocateArtwork(movie, ArtworkKind.Poster).IfSomeAsync(
async posterFile => async posterFile =>
{ {
MovieMetadata metadata = movie.MovieMetadata.Head(); MovieMetadata metadata = movie.MovieMetadata.Head();
@ -157,6 +158,28 @@ namespace ErsatzTV.Core.Metadata
} }
} }
private async Task<Either<BaseError, Movie>> UpdateFanArt(Movie movie)
{
try
{
await LocateArtwork(movie, ArtworkKind.FanArt).IfSomeAsync(
async posterFile =>
{
MovieMetadata metadata = movie.MovieMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.FanArt))
{
await _movieRepository.Update(movie);
}
});
return movie;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
private Option<string> LocateNfoFile(Movie movie) private Option<string> LocateNfoFile(Movie movie)
{ {
string path = movie.MediaVersions.Head().MediaFiles.Head().Path; string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
@ -167,12 +190,19 @@ namespace ErsatzTV.Core.Metadata
.HeadOrNone(); .HeadOrNone();
} }
private Option<string> LocatePoster(Movie movie) private Option<string> LocateArtwork(Movie movie, ArtworkKind artworkKind)
{ {
string segment = artworkKind switch
{
ArtworkKind.Poster => "poster",
ArtworkKind.FanArt => "fanart",
_ => throw new ArgumentOutOfRangeException(nameof(artworkKind))
};
string path = movie.MediaVersions.Head().MediaFiles.Head().Path; string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
string folder = Path.GetDirectoryName(path) ?? string.Empty; string folder = Path.GetDirectoryName(path) ?? string.Empty;
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect( IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect(
ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(path) + $"-poster.{ext}" }) ext => new[] { $"{segment}.{ext}", Path.GetFileNameWithoutExtension(path) + $"-{segment}.{ext}" })
.Map(f => Path.Combine(folder, f)); .Map(f => Path.Combine(folder, f));
Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone(); Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone();
return result; return result;

2
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -56,6 +56,7 @@ namespace ErsatzTV.Infrastructure.Images
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder), ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder), ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder), ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder _ => FileSystemLayout.LegacyImageCacheFolder
}; };
string target = Path.Combine(baseFolder, hex); string target = Path.Combine(baseFolder, hex);
@ -85,6 +86,7 @@ namespace ErsatzTV.Infrastructure.Images
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder), ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder), ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder), ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder _ => FileSystemLayout.LegacyImageCacheFolder
}; };
string target = Path.Combine(baseFolder, hex); string target = Path.Combine(baseFolder, hex);

1
ErsatzTV.sln.DotSettings

@ -14,6 +14,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=drawtext/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=drawtext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ersatztv/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=ersatztv/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=etvignore/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=etvignore/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=fanart/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=faststart/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=faststart/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=featurette/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=featurette/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=featurettes/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=featurettes/@EntryIndexedValue">True</s:Boolean>

11
ErsatzTV/Controllers/ArtworkController.cs

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers namespace ErsatzTV.Controllers
{ {
[ResponseCache(Duration = 3600)]
[ApiController] [ApiController]
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
public class PostersController : ControllerBase public class PostersController : ControllerBase
@ -37,6 +38,16 @@ namespace ErsatzTV.Controllers
Right: r => new FileContentResult(r.Contents, r.MimeType)); Right: r => new FileContentResult(r.Contents, r.MimeType));
} }
[HttpGet("/artwork/fanart/{fileName}")]
public async Task<IActionResult> GetFanArt(string fileName)
{
Either<BaseError, ImageViewModel> imageContents =
await _mediator.Send(new GetImageContents(fileName, ArtworkKind.FanArt));
return imageContents.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new FileContentResult(r.Contents, r.MimeType));
}
[HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")] [HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
public async Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path) public async Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path)
{ {

94
ErsatzTV/Pages/ChannelEditor.razor

@ -10,55 +10,57 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<div style="max-width: 400px;"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Channel" : "Add Channel")</MudText> <div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Channel" : "Add Channel")</MudText>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/> <FluentValidator/>
<MudCard> <MudCard>
<MudCardContent> <MudCardContent>
<MudTextField Label="Number" @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/> <MudTextField Label="Number" @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)"> <MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
@foreach (StreamingMode streamingMode in Enum.GetValues<StreamingMode>()) @foreach (StreamingMode streamingMode in Enum.GetValues<StreamingMode>())
{
<MudSelectItem Value="@streamingMode">@streamingMode</MudSelectItem>
}
</MudSelect>
<MudSelect Class="mt-3" Label="FFmpeg Profile" @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
Disabled="@(_model.StreamingMode != StreamingMode.TransportStream)">
@foreach (FFmpegProfileViewModel profile in _ffmpegProfiles)
{
<MudSelectItem Value="@profile.Id">@profile.Name</MudSelectItem>
}
</MudSelect>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<InputFile id="fileInput" OnChange="UploadLogo" hidden/>
@if (!string.IsNullOrWhiteSpace(_model.Logo))
{ {
<MudElement HtmlTag="img" src="@($"iptv/logos/{_model.Logo}")" Style="max-height: 50px"/> <MudSelectItem Value="@streamingMode">@streamingMode</MudSelectItem>
} }
</MudItem> </MudSelect>
<MudItem xs="6"> <MudSelect Class="mt-3" Label="FFmpeg Profile" @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
<MudButton Class="ml-auto" HtmlTag="label" Disabled="@(_model.StreamingMode != StreamingMode.TransportStream)">
Variant="Variant.Filled" @foreach (FFmpegProfileViewModel profile in _ffmpegProfiles)
Color="Color.Primary" {
StartIcon="@Icons.Material.Filled.CloudUpload" <MudSelectItem Value="@profile.Id">@profile.Name</MudSelectItem>
for="fileInput"> }
Upload Logo </MudSelect>
</MudButton> <MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
</MudItem> <MudItem xs="6">
</MudGrid> <InputFile id="fileInput" OnChange="UploadLogo" hidden/>
</MudCardContent> @if (!string.IsNullOrWhiteSpace(_model.Logo))
<MudCardActions> {
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary"> <MudElement HtmlTag="img" src="@($"iptv/logos/{_model.Logo}")" Style="max-height: 50px"/>
@(IsEdit ? "Save Changes" : "Add Channel") }
</MudButton> </MudItem>
</MudCardActions> <MudItem xs="6">
</MudCard> <MudButton Class="ml-auto" HtmlTag="label"
</EditForm> Variant="Variant.Filled"
</div> Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="fileInput">
Upload Logo
</MudButton>
</MudItem>
</MudGrid>
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
@(IsEdit ? "Save Changes" : "Add Channel")
</MudButton>
</MudCardActions>
</MudCard>
</EditForm>
</div>
</MudContainer>
@code { @code {

24
ErsatzTV/Pages/Channels.razor

@ -7,7 +7,7 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_channels"> <MudTable Hover="true" Items="_channels">
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">Channels</MudText> <MudText Typo="Typo.h6">Channels</MudText>
@ -18,7 +18,7 @@
<col style="width: 20%"/> <col style="width: 20%"/>
<col style="width: 20%"/> <col style="width: 20%"/>
<col style="width: 20%"/> <col style="width: 20%"/>
<col style="width: 60px;"/> <col style="width: 120px;"/>
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh> <MudTh>
@ -49,14 +49,18 @@
} }
</MudTd> </MudTd>
<MudTd> <MudTd>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Direction="Direction.Left" OffsetX="true"> <div style="align-items: center; display: flex;">
<MudMenuItem Icon="@Icons.Material.Filled.Edit" Link="@($"/channels/{context.Id}")"> <MudTooltip Text="Edit Channel">
Edit <MudIconButton Icon="@Icons.Material.Filled.Edit"
</MudMenuItem> Link="@($"/channels/{context.Id}")">
<MudMenuItem Icon="@Icons.Material.Filled.Delete" OnClick="@(_ => DeleteChannelAsync(context))"> </MudIconButton>
Delete </MudTooltip>
</MudMenuItem> <MudTooltip Text="Delete Channel">
</MudMenu> <MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteChannelAsync(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>

34
ErsatzTV/Pages/CollectionEditor.razor

@ -8,23 +8,25 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<div style="max-width: 400px;"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Collection" : "Add Collection")</MudText> <div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Collection" : "Add Collection")</MudText>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/> <FluentValidator/>
<MudCard> <MudCard>
<MudCardContent> <MudCardContent>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto"> <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">
@(IsEdit ? "Save Changes" : "Add Collection") @(IsEdit ? "Save Changes" : "Add Collection")
</MudButton> </MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
</EditForm> </EditForm>
</div> </div>
</MudContainer>
@code { @code {

128
ErsatzTV/Pages/CollectionItems.razor

@ -9,75 +9,77 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<div class="mb-6" style="display: flex; flex-direction: row;"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText GutterBottom="true" Typo="Typo.h2">@_data.Name</MudText> <div class="mb-6" style="display: flex; flex-direction: row;">
<MudIconButton Icon="@Icons.Material.Filled.Edit" <MudText GutterBottom="true" Typo="Typo.h2">@_data.Name</MudText>
Link="@($"/media/collections/{Id}/edit")" <MudIconButton Icon="@Icons.Material.Filled.Edit"
Style="margin-bottom: auto; margin-top: auto;"/> Link="@($"/media/collections/{Id}/edit")"
</div> Style="margin-bottom: auto; margin-top: auto;"/>
</div>
@if (_data.MovieCards.Any())
{ @if (_data.MovieCards.Any())
<MudText GutterBottom="true" Typo="Typo.h4">Movies</MudText> {
<MudText GutterBottom="true" Typo="Typo.h4">Movies</MudText>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MovieCardViewModel card in _data.MovieCards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Link="@($"/media/movies/{card.MovieId}")"
DeleteClicked="@RemoveMovieFromCollection"/>
}
</MudContainer>
}
@if (_data.ShowCards.Any()) <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
{ @foreach (MovieCardViewModel card in _data.MovieCards.OrderBy(m => m.SortTitle))
<MudText GutterBottom="true" Typo="Typo.h4">Television Shows</MudText> {
<MediaCard Data="@card"
Link="@($"/media/movies/{card.MovieId}")"
DeleteClicked="@RemoveMovieFromCollection"/>
}
</MudContainer>
}
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> @if (_data.ShowCards.Any())
@foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle)) {
{ <MudText GutterBottom="true" Typo="Typo.h4">Television Shows</MudText>
<MediaCard Data="@card"
Link="@($"/media/tv/shows/{card.TelevisionShowId}")"
DeleteClicked="@RemoveShowFromCollection"/>
}
</MudContainer>
}
@if (_data.SeasonCards.Any()) <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
{ @foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle))
<MudText GutterBottom="true" Typo="Typo.h4">Television Seasons</MudText> {
<MediaCard Data="@card"
Link="@($"/media/tv/shows/{card.TelevisionShowId}")"
DeleteClicked="@RemoveShowFromCollection"/>
}
</MudContainer>
}
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> @if (_data.SeasonCards.Any())
@foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle)) {
{ <MudText GutterBottom="true" Typo="Typo.h4">Television Seasons</MudText>
<MediaCard Data="@card"
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
Title="@card.ShowTitle"
Subtitle="@card.Title"
DeleteClicked="@RemoveSeasonFromCollection"/>
}
</MudContainer>
}
@if (_data.EpisodeCards.Any()) <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
{ @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle))
<MudText GutterBottom="true" Typo="Typo.h4">Television Episodes</MudText> {
<MediaCard Data="@card"
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
Title="@card.ShowTitle"
Subtitle="@card.Title"
DeleteClicked="@RemoveSeasonFromCollection"/>
}
</MudContainer>
}
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> @if (_data.EpisodeCards.Any())
@foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired)) {
{ <MudText GutterBottom="true" Typo="Typo.h4">Television Episodes</MudText>
<MediaCard Data="@card"
Link="@($"/media/tv/episodes/{card.EpisodeId}")" <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
Title="@card.ShowTitle" @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired))
Subtitle="@card.Title" {
ContainerClass="media-card-episode-container mx-2" <MediaCard Data="@card"
CardClass="media-card-episode" Link="@($"/media/tv/episodes/{card.EpisodeId}")"
DeleteClicked="@RemoveEpisodeFromCollection" Title="@card.ShowTitle"
ArtworkKind="@ArtworkKind.Thumbnail"/> Subtitle="@card.Title"
} ContainerClass="media-card-episode-container mx-2"
</MudContainer> CardClass="media-card-episode"
} DeleteClicked="@RemoveEpisodeFromCollection"
ArtworkKind="@ArtworkKind.Thumbnail"/>
}
</MudContainer>
}
</MudContainer>
@code { @code {

30
ErsatzTV/Pages/Collections.razor

@ -6,21 +6,23 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@foreach (MediaCollectionViewModel card in _data) <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
{ @foreach (MediaCollectionViewModel card in _data)
<MediaCard Data="@card" {
Link="@($"/media/collections/{card.Id}")" <MediaCard Data="@card"
ContainerClass="media-card-episode-container mr-4" Link="@($"/media/collections/{card.Id}")"
CardClass="media-card-episode" ContainerClass="media-card-episode-container mr-4"
DeleteClicked="@DeleteMediaCollection"/> CardClass="media-card-episode"
} DeleteClicked="@DeleteMediaCollection"/>
</MudContainer> }
</MudContainer>
<MudContainer MaxWidth="MaxWidth.False"> <MudContainer MaxWidth="MaxWidth.False">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/media/collections/add" Class="mt-4"> <MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/media/collections/add" Class="mt-4">
Add Collection Add Collection
</MudButton> </MudButton>
</MudContainer>
</MudContainer> </MudContainer>
@code { @code {

32
ErsatzTV/Pages/FFmpeg.razor

@ -5,7 +5,7 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCard> <MudCard>
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
@ -37,7 +37,7 @@
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">FFmpeg Profiles</MudText> <MudText Typo="Typo.h6">FFmpeg Profiles</MudText>
<MudToolBarSpacer></MudToolBarSpacer> <MudToolBarSpacer></MudToolBarSpacer>
<MudText Color="Color.Primary">Colored settings will be normalized</MudText> <MudText Color="Color.Tertiary">Colored settings will be normalized</MudText>
</ToolBarContent> </ToolBarContent>
<ColGroup> <ColGroup>
<col/> <col/>
@ -45,7 +45,7 @@
<col/> <col/>
<col/> <col/>
<col/> <col/>
<col style="width: 60px;"/> <col style="width: 120px;"/>
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
@ -61,29 +61,33 @@
@(context.Transcode ? "Yes" : "No") @(context.Transcode ? "Yes" : "No")
</MudTd> </MudTd>
<MudTd DataLabel="Resolution"> <MudTd DataLabel="Resolution">
<MudText Color="@(context.Transcode && context.NormalizeResolution ? Color.Primary : Color.Inherit)"> <MudText Color="@(context.Transcode && context.NormalizeResolution ? Color.Tertiary : Color.Inherit)">
@context.Resolution.Name @context.Resolution.Name
</MudText> </MudText>
</MudTd> </MudTd>
<MudTd DataLabel="Video Codec"> <MudTd DataLabel="Video Codec">
<MudText Color="@(context.Transcode && context.NormalizeVideoCodec ? Color.Primary : Color.Inherit)"> <MudText Color="@(context.Transcode && context.NormalizeVideoCodec ? Color.Tertiary : Color.Inherit)">
@context.VideoCodec @context.VideoCodec
</MudText> </MudText>
</MudTd> </MudTd>
<MudTd DataLabel="Audio Codec"> <MudTd DataLabel="Audio Codec">
<MudText Color="@(context.Transcode && context.NormalizeAudioCodec ? Color.Primary : Color.Inherit)"> <MudText Color="@(context.Transcode && context.NormalizeAudioCodec ? Color.Tertiary : Color.Inherit)">
@context.AudioCodec @context.AudioCodec
</MudText> </MudText>
</MudTd> </MudTd>
<MudTd> <MudTd>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Direction="Direction.Left" OffsetX="true"> <div style="align-items: center; display: flex;">
<MudMenuItem Icon="@Icons.Material.Filled.Edit" Link="@($"/ffmpeg/{context.Id}")"> <MudTooltip Text="Edit Channel">
Edit <MudIconButton Icon="@Icons.Material.Filled.Edit"
</MudMenuItem> Link="@($"/ffmpeg/{context.Id}")">
<MudMenuItem Icon="@Icons.Material.Filled.Delete" OnClick="@(_ => DeleteProfileAsync(context))"> </MudIconButton>
Delete </MudTooltip>
</MudMenuItem> <MudTooltip Text="Delete Channel">
</MudMenu> <MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteProfileAsync(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>

185
ErsatzTV/Pages/FFmpegEditor.razor

@ -10,98 +10,99 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<FluentValidator/> <EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<MudCard> <FluentValidator/>
<MudCardHeader> <MudCard>
<CardHeaderContent> <MudCardHeader>
<MudText Typo="Typo.h5">@(IsEdit ? "Edit FFmpeg Profile" : "Add FFmpeg Profile")</MudText> <CardHeaderContent>
</CardHeaderContent> <MudText Typo="Typo.h5">@(IsEdit ? "Edit FFmpeg Profile" : "Add FFmpeg Profile")</MudText>
</MudCardHeader> </CardHeaderContent>
<MudCardContent> </MudCardHeader>
<MudGrid> <MudCardContent>
<MudItem xs="12"> <MudGrid>
<MudGrid Spacing="4" Justify="Justify.Center"> <MudItem xs="12">
<MudItem> <MudGrid Spacing="4" Justify="Justify.Center">
<MudText Typo="Typo.h6">General</MudText> <MudItem>
<MudTextField Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudText Typo="Typo.h6">General</MudText>
<MudElement HtmlTag="div" Class="mt-3"> <MudTextField Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudTextField Label="Thread Count" @bind-Value="@_model.ThreadCount" For="@(() => _model.ThreadCount)"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Label="Thread Count" @bind-Value="@_model.ThreadCount" For="@(() => _model.ThreadCount)"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudSelect Disabled="@(!_model.Transcode)" Label="Preferred Resolution" @bind-Value="_model.Resolution" For="@(() => _model.Resolution)"> <MudElement HtmlTag="div" Class="mt-3">
@foreach (ResolutionViewModel resolution in _resolutions) <MudSelect Disabled="@(!_model.Transcode)" Label="Preferred Resolution" @bind-Value="_model.Resolution" For="@(() => _model.Resolution)">
{ @foreach (ResolutionViewModel resolution in _resolutions)
<MudSelectItem Value="@resolution">@resolution.Name</MudSelectItem> {
} <MudSelectItem Value="@resolution">@resolution.Name</MudSelectItem>
</MudSelect> }
</MudElement> </MudSelect>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudCheckBox Label="Transcode" @bind-Checked="@_model.Transcode" For="@(() => _model.Transcode)"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudCheckBox Label="Transcode" @bind-Checked="@_model.Transcode" For="@(() => _model.Transcode)"/>
</MudItem> </MudElement>
<MudItem> </MudItem>
<MudText Typo="Typo.h6">Video</MudText> <MudItem>
<MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.VideoCodec" For="@(() => _model.VideoCodec)"/> <MudText Typo="Typo.h6">Video</MudText>
<MudElement HtmlTag="div" Class="mt-3"> <MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.VideoCodec" For="@(() => _model.VideoCodec)"/>
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudTextField Disabled="@(!_model.Transcode)" Label="Buffer Size" @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Disabled="@(!_model.Transcode)" Label="Buffer Size" @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudSelect Disabled="@(!_model.Transcode)" Label="Hardware Acceleration" @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)"> <MudElement HtmlTag="div" Class="mt-3">
@foreach (HardwareAccelerationKind hwAccel in Enum.GetValues<HardwareAccelerationKind>()) <MudSelect Disabled="@(!_model.Transcode)" Label="Hardware Acceleration" @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
{ @foreach (HardwareAccelerationKind hwAccel in Enum.GetValues<HardwareAccelerationKind>())
<MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem> {
} <MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
</MudSelect> }
</MudElement> </MudSelect>
</MudItem> </MudElement>
<MudItem> </MudItem>
<MudText Typo="Typo.h6">Audio</MudText> <MudItem>
<MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.AudioCodec" For="@(() => _model.AudioCodec)"/> <MudText Typo="Typo.h6">Audio</MudText>
<MudElement HtmlTag="div" Class="mt-3"> <MudTextField Disabled="@(!_model.Transcode)" Label="Codec" @bind-Value="_model.AudioCodec" For="@(() => _model.AudioCodec)"/>
<MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Disabled="@(!_model.Transcode)" Label="Bitrate" @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudTextField Disabled="@(!_model.Transcode)" Label="Buffer Size" @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Disabled="@(!_model.Transcode)" Label="Buffer Size" @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudTextField Disabled="@(!_model.Transcode)" Label="Volume" @bind-Value="_model.AudioVolume" For="@(() => _model.AudioVolume)" Adornment="Adornment.End" AdornmentText="%"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Disabled="@(!_model.Transcode)" Label="Volume" @bind-Value="_model.AudioVolume" For="@(() => _model.AudioVolume)" Adornment="Adornment.End" AdornmentText="%"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudTextField Disabled="@(!_model.Transcode)" Label="Channels" @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Disabled="@(!_model.Transcode)" Label="Channels" @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudTextField Disabled="@(!_model.Transcode)" Label="Sample Rate" @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudTextField Disabled="@(!_model.Transcode)" Label="Sample Rate" @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
</MudItem> </MudElement>
<MudItem> </MudItem>
<MudText Typo="Typo.h6">Normalization</MudText> <MudItem>
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Resolution" @bind-Checked="@_model.NormalizeResolution" For="@(() => _model.NormalizeResolution)"/> <MudText Typo="Typo.h6">Normalization</MudText>
<MudElement HtmlTag="div" Class="mt-3"> <MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Resolution" @bind-Checked="@_model.NormalizeResolution" For="@(() => _model.NormalizeResolution)"/>
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video Codec" @bind-Checked="@_model.NormalizeVideoCodec" For="@(() => _model.NormalizeVideoCodec)"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video Codec" @bind-Checked="@_model.NormalizeVideoCodec" For="@(() => _model.NormalizeVideoCodec)"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio Codec" @bind-Checked="@_model.NormalizeAudioCodec" For="@(() => _model.NormalizeAudioCodec)"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio Codec" @bind-Checked="@_model.NormalizeAudioCodec" For="@(() => _model.NormalizeAudioCodec)"/>
<MudElement HtmlTag="div" Class="mt-3"> </MudElement>
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio" @bind-Checked="@_model.NormalizeAudio" For="@(() => _model.NormalizeAudio)"/> <MudElement HtmlTag="div" Class="mt-3">
</MudElement> <MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio" @bind-Checked="@_model.NormalizeAudio" For="@(() => _model.NormalizeAudio)"/>
</MudItem> </MudElement>
</MudGrid> </MudItem>
</MudItem> </MudGrid>
</MudGrid> </MudItem>
</MudCardContent> </MudGrid>
<MudCardActions> </MudCardContent>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary"> <MudCardActions>
@(IsEdit ? "Save Changes" : "Add Profile") <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
</MudButton> @(IsEdit ? "Save Changes" : "Add Profile")
</MudCardActions> </MudButton>
</MudCard> </MudCardActions>
</MudCard>
</EditForm> </EditForm>
</MudContainer>
@code { @code {

138
ErsatzTV/Pages/Index.razor

@ -1,70 +1,72 @@
@page "/" @page "/"
<MudCard> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCardContent> <MudCard>
<MudText Typo="Typo.h3">Welcome to ErsatzTV!</MudText> <MudCardContent>
<MudElement HtmlTag="div" Class="mt-6"> <MudText Typo="Typo.h3">Welcome to ErsatzTV!</MudText>
<MudText Typo="Typo.h4" GutterBottom="true">Channels</MudText> <MudElement HtmlTag="div" Class="mt-6">
<MudText> <MudText Typo="Typo.h4" GutterBottom="true">Channels</MudText>
<MudLink Href="/channels">Channels</MudLink> are not directly associated with any media. Channels have a <b>number</b>, a <b>name</b>, and a <b>streaming mode</b> that indicates how the channel will play media. <MudText>
</MudText> <MudLink Href="/channels">Channels</MudLink> are not directly associated with any media. Channels have a <b>number</b>, a <b>name</b>, and a <b>streaming mode</b> that indicates how the channel will play media.
<MudText Class="mt-3"> </MudText>
In <b>TransportStream</b> mode, the channel will also require an <b>FFmpeg profile</b> to configure transcoding and normalization. <MudText Class="mt-3">
In <b>HttpLiveStreaming</b> mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format. In <b>TransportStream</b> mode, the channel will also require an <b>FFmpeg profile</b> to configure transcoding and normalization.
</MudText> In <b>HttpLiveStreaming</b> mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format.
</MudElement> </MudText>
<MudElement HtmlTag="div" Class="mt-6"> </MudElement>
<MudText Typo="Typo.h4" GutterBottom="true">FFmpeg Profiles</MudText> <MudElement HtmlTag="div" Class="mt-6">
<MudText> <MudText Typo="Typo.h4" GutterBottom="true">FFmpeg Profiles</MudText>
<MudLink Href="/ffmpeg">FFmpeg Profiles</MudLink> are collections of FFmpeg settings that are applied at the channel level. <MudText>
All content on a given channel will use the same FFmpeg settings. This also means the same content on different channels can use different settings. <MudLink Href="/ffmpeg">FFmpeg Profiles</MudLink> are collections of FFmpeg settings that are applied at the channel level.
</MudText> All content on a given channel will use the same FFmpeg settings. This also means the same content on different channels can use different settings.
</MudElement> </MudText>
<MudElement HtmlTag="div" Class="mt-6"> </MudElement>
<MudText Typo="Typo.h4" GutterBottom="true">Libraries</MudText> <MudElement HtmlTag="div" Class="mt-6">
<MudText> <MudText Typo="Typo.h4" GutterBottom="true">Libraries</MudText>
Two local <MudLink Href="/media/libraries">libraries</MudLink> are available, one for each <b>media kind</b>: Shows and Movies. Libraries contain <b>paths</b> (folders) to regularly scan for media items. <MudText>
Support for Plex libraries is under active development; Jellyfin and Emby library support is planned. Two local <MudLink Href="/media/libraries">libraries</MudLink> are available, one for each <b>media kind</b>: Shows and Movies. Libraries contain <b>paths</b> (folders) to regularly scan for media items.
</MudText> Support for Plex libraries is under active development; Jellyfin and Emby library support is planned.
</MudElement> </MudText>
<MudElement HtmlTag="div" Class="mt-6"> </MudElement>
<MudText Typo="Typo.h4" GutterBottom="true">Collections</MudText> <MudElement HtmlTag="div" Class="mt-6">
<MudText> <MudText Typo="Typo.h4" GutterBottom="true">Collections</MudText>
<MudLink Href="/media/collections">Collections</MudLink> have a <b>name</b> and contain a logical grouping of media items. <MudText>
Collections may contain shows, seasons, episodes or movies. <MudLink Href="/media/collections">Collections</MudLink> have a <b>name</b> and contain a logical grouping of media items.
Collections containing shows and seasons are automatically updated as media is added or removed from the linked shows and seasons. Collections may contain shows, seasons, episodes or movies.
</MudText> Collections containing shows and seasons are automatically updated as media is added or removed from the linked shows and seasons.
</MudElement> </MudText>
<MudElement HtmlTag="div" Class="mt-6"> </MudElement>
<MudText Typo="Typo.h4" GutterBottom="true">Schedules</MudText> <MudElement HtmlTag="div" Class="mt-6">
<MudText> <MudText Typo="Typo.h4" GutterBottom="true">Schedules</MudText>
<MudLink Href="/schedules">Schedules</MudLink> have a <b>name</b>, a <b>collection playback order</b> and <b>items</b> to continually loop through. <MudText>
</MudText> <MudLink Href="/schedules">Schedules</MudLink> have a <b>name</b>, a <b>collection playback order</b> and <b>items</b> to continually loop through.
<MudText Class="mt-3 mb-2">Three <b>collection playback orders</b> are supported:</MudText> </MudText>
<ul class="mud-typography-body1"> <MudText Class="mt-3 mb-2">Three <b>collection playback orders</b> are supported:</MudText>
<li><b>Random</b> - to randomly play collection items; repeating is allowed before all collection items have been played.</li> <ul class="mud-typography-body1">
<li><b>Shuffle</b> - to randomly play collection items; repeating is <i>not</i> allowed until all collection items have been played.</li> <li><b>Random</b> - to randomly play collection items; repeating is allowed before all collection items have been played.</li>
<li><b>Chronological</b> - to play collection items sorted by air date and then by season and episode number (for when multiple episodes aired on a single day).</li> <li><b>Shuffle</b> - to randomly play collection items; repeating is <i>not</i> allowed until all collection items have been played.</li>
</ul> <li><b>Chronological</b> - to play collection items sorted by air date and then by season and episode number (for when multiple episodes aired on a single day).</li>
<MudText Class="mt-3"> </ul>
Schedule items have a <b>start type</b>, a <b>start time</b>, a <b>collection</b> and a <b>playout mode</b>. <MudText Class="mt-3">
</MudText> Schedule items have a <b>start type</b>, a <b>start time</b>, a <b>collection</b> and a <b>playout mode</b>.
<MudText Class="mt-3"> </MudText>
A <b>fixed</b> start type requires a <b>start time</b>, while a <b>dynamic</b> start type means the schedule item will start immediately after the preceding schedule item. <MudText Class="mt-3">
</MudText> A <b>fixed</b> start type requires a <b>start time</b>, while a <b>dynamic</b> start type means the schedule item will start immediately after the preceding schedule item.
<MudText Class="mt-3 mb-2">Four <b>playout modes</b> are supported:</MudText> </MudText>
<ul class="mud-typography-body1"> <MudText Class="mt-3 mb-2">Four <b>playout modes</b> are supported:</MudText>
<li><b>One</b> - to play one media item from the collection before advancing to the next schedule item.</li> <ul class="mud-typography-body1">
<li><b>Multiple</b> - to play a specified <b>count</b> of media items from the collection before advancing to the next schedule item.</li> <li><b>One</b> - to play one media item from the collection before advancing to the next schedule item.</li>
<li><b>Duration</b> - to play the maximum number of complete media items that will fit in the specified <b>playout duration</b>, before either going offline for the remainder of the <b>playout duration</b> (an <b>offline tail</b>), or immediately advancing to the next schedule item.</li> <li><b>Multiple</b> - to play a specified <b>count</b> of media items from the collection before advancing to the next schedule item.</li>
<li><b>Flood</b> - to play media items from the collection forever, or until the next schedule item's <b>start time</b> if one exists.</li> <li><b>Duration</b> - to play the maximum number of complete media items that will fit in the specified <b>playout duration</b>, before either going offline for the remainder of the <b>playout duration</b> (an <b>offline tail</b>), or immediately advancing to the next schedule item.</li>
</ul> <li><b>Flood</b> - to play media items from the collection forever, or until the next schedule item's <b>start time</b> if one exists.</li>
</MudElement> </ul>
<MudElement HtmlTag="div" Class="mt-6"> </MudElement>
<MudText Typo="Typo.h4" GutterBottom="true">Playouts</MudText> <MudElement HtmlTag="div" Class="mt-6">
<MudText> <MudText Typo="Typo.h4" GutterBottom="true">Playouts</MudText>
<MudLink Href="/playouts">Playouts</MudLink> assign a <b>schedule</b> to a <b>channel</b> and individually track the ordered playback of collection items. <MudText>
</MudText> <MudLink Href="/playouts">Playouts</MudLink> assign a <b>schedule</b> to a <b>channel</b> and individually track the ordered playback of collection items.
</MudElement> </MudText>
</MudCardContent> </MudElement>
</MudCard> </MudCardContent>
</MudCard>
</MudContainer>

2
ErsatzTV/Pages/Libraries.razor

@ -8,7 +8,7 @@
@inject IEntityLocker Locker @inject IEntityLocker Locker
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_libraries" Dense="true"> <MudTable Hover="true" Items="_libraries" Dense="true">
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">Libraries</MudText> <MudText Typo="Typo.h6">Libraries</MudText>

2
ErsatzTV/Pages/LocalLibraryEditor.razor

@ -5,7 +5,7 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_libraryPaths" Dense="true"> <MudTable Hover="true" Items="_libraryPaths" Dense="true">
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">Library Paths</MudText> <MudText Typo="Typo.h6">Library Paths</MudText>

2
ErsatzTV/Pages/LocalLibraryPathEditor.razor

@ -10,7 +10,7 @@
@inject IEntityLocker Locker @inject IEntityLocker Locker
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">@_library.Name - Add Local Library Path</MudText> <MudText Typo="Typo.h4" Class="mb-4">@_library.Name - Add Local Library Path</MudText>
<div style="max-width: 400px;"> <div style="max-width: 400px;">
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">

2
ErsatzTV/Pages/Logs.razor

@ -3,7 +3,7 @@
@using ErsatzTV.Application.Logs.Queries @using ErsatzTV.Application.Logs.Queries
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable FixedHeader="true" Dense="true" Items="_logEntries"> <MudTable FixedHeader="true" Dense="true" Items="_logEntries">
<HeaderContent> <HeaderContent>
<MudTh>Timestamp</MudTh> <MudTh>Timestamp</MudTh>

57
ErsatzTV/Pages/Movie.razor

@ -7,33 +7,42 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs> <div class="fanart-tint"></div>
<MudCard Class="mb-6"> @if (!string.IsNullOrWhiteSpace(_movie.FanArt))
<div style="display: flex; flex-direction: row;"> {
@if (!string.IsNullOrWhiteSpace(_movie.Poster)) <img src="@($"/artwork/fanart/{_movie.FanArt}")" alt="fan art"/>
}
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Style="margin-top: 200px">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_movie.Poster))
{
<img class="mud-elevation-2 mr-6"
style="border-radius: 4px; max-height: 440px"
src="@($"/artwork/posters/{_movie.Poster}")" alt="movie poster"/>
}
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h2" Class="media-item-title">@_movie.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="media-item-subtitle mb-6 mud-text-secondary">@_movie.Year</MudText>
@if (!string.IsNullOrWhiteSpace(_movie.Plot))
{ {
<MudPaper Style="flex-shrink: 0;"> <MudCard Elevation="2" Class="mb-6">
<MudCardMedia Image="@($"/artwork/posters/{_movie.Poster}")" Style="height: 440px; width: 304px;"/> <MudCardContent Class="mx-3 my-3" Style="height: 100%">
</MudPaper> <MudText Style="flex-grow: 1">@_movie.Plot</MudText>
</MudCardContent>
</MudCard>
} }
<MudCardContent Class="mx-3 my-3"> <div>
<div style="display: flex; flex-direction: column; height: 100%"> <MudButton Variant="Variant.Filled"
<MudText Typo="Typo.h3">@_movie.Title</MudText> Color="Color.Primary"
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_movie.Year</MudText> StartIcon="@Icons.Material.Filled.Add"
<MudText Style="flex-grow: 1">@_movie.Plot</MudText> OnClick="@AddToCollection">
<div> Add To Collection
<MudButton Variant="Variant.Filled" </MudButton>
Color="Color.Primary" </div>
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
</div>
</div>
</MudCardContent>
</div> </div>
</MudCard> </div>
</MudContainer> </MudContainer>
@code { @code {

46
ErsatzTV/Pages/MovieList.razor

@ -12,29 +12,31 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudPaper Style="align-items: center; display: flex; justify-content: center;"> <MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px">
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft" <MudPaper Style="align-items: center; display: flex; justify-content: center;">
OnClick="@PrevPage" <MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
Disabled="@(PageNumber <= 1)"> OnClick="@PrevPage"
</MudIconButton> Disabled="@(PageNumber <= 1)">
<MudText Style="flex-grow: 1" </MudIconButton>
Align="Align.Center"> <MudText Style="flex-grow: 1"
@Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count Align="Align.Center">
</MudText> @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight" </MudText>
OnClick="@NextPage" Disabled="@(PageNumber * PageSize >= _data.Count)"> <MudIconButton Icon="@Icons.Material.Outlined.ChevronRight"
</MudIconButton> OnClick="@NextPage" Disabled="@(PageNumber * PageSize >= _data.Count)">
</MudPaper> </MudIconButton>
</MudContainer> </MudPaper>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MovieCardViewModel card in _data.Cards.Where(d => !string.IsNullOrWhiteSpace(d.Title))) @foreach (MovieCardViewModel card in _data.Cards.Where(d => !string.IsNullOrWhiteSpace(d.Title)))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
Link="@($"/media/movies/{card.MovieId}")" Link="@($"/media/movies/{card.MovieId}")"
AddToCollectionClicked="@AddToCollection"/> AddToCollectionClicked="@AddToCollection"/>
} }
</MudContainer>
</MudContainer> </MudContainer>
@code { @code {

36
ErsatzTV/Pages/PlayoutEditor.razor

@ -8,24 +8,26 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<div style="max-width: 400px;"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Add Playout</MudText> <div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">Add Playout</MudText>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/> <FluentValidator/>
<MudCard> <MudCard>
<MudCardContent> <MudCardContent>
<MudAutocomplete T="ChannelViewModel" Label="Channel" @bind-value="_model.Channel" SearchFunc="@SearchChannels" ToStringFunc="@(c => c is null ? null : $"{c.Number} - {c.Name}")"/> <MudAutocomplete T="ChannelViewModel" Label="Channel" @bind-value="_model.Channel" SearchFunc="@SearchChannels" ToStringFunc="@(c => c is null ? null : $"{c.Number} - {c.Name}")"/>
<MudAutocomplete Class="mt-3" T="ProgramScheduleViewModel" Label="Schedule" @bind-value="_model.ProgramSchedule" SearchFunc="@SearchProgramSchedules" ToStringFunc="@(s => s?.Name)"/> <MudAutocomplete Class="mt-3" T="ProgramScheduleViewModel" Label="Schedule" @bind-value="_model.ProgramSchedule" SearchFunc="@SearchProgramSchedules" ToStringFunc="@(s => s?.Name)"/>
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary"> <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
Add Playout Add Playout
</MudButton> </MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
</EditForm> </EditForm>
</div> </div>
</MudContainer>
@code { @code {

2
ErsatzTV/Pages/Playouts.razor

@ -5,7 +5,7 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Dense="true" Items="_playouts" SelectedItemChanged="@(async (PlayoutViewModel x) => await PlayoutSelected(x))"> <MudTable Hover="true" Dense="true" Items="_playouts" SelectedItemChanged="@(async (PlayoutViewModel x) => await PlayoutSelected(x))">
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">Playouts</MudText> <MudText Typo="Typo.h6">Playouts</MudText>

46
ErsatzTV/Pages/ScheduleEditor.razor

@ -7,29 +7,31 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<div style="max-width: 400px;"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Schedule" : "Add Schedule")</MudText> <div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Schedule" : "Add Schedule")</MudText>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/> <FluentValidator/>
<MudCard> <MudCard>
<MudCardContent> <MudCardContent>
<MudTextField Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudTextField Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudSelect Class="mt-3" Label="Collection Playback Order" @bind-Value="_model.MediaCollectionPlaybackOrder" For="@(() => _model.MediaCollectionPlaybackOrder)"> <MudSelect Class="mt-3" Label="Collection Playback Order" @bind-Value="_model.MediaCollectionPlaybackOrder" For="@(() => _model.MediaCollectionPlaybackOrder)">
@foreach (PlaybackOrder playbackOrder in Enum.GetValues<PlaybackOrder>()) @foreach (PlaybackOrder playbackOrder in Enum.GetValues<PlaybackOrder>())
{ {
<MudSelectItem Value="@playbackOrder">@playbackOrder</MudSelectItem> <MudSelectItem Value="@playbackOrder">@playbackOrder</MudSelectItem>
} }
</MudSelect> </MudSelect>
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary"> <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
@(IsEdit ? "Save Changes" : "Add Schedule") @(IsEdit ? "Save Changes" : "Add Schedule")
</MudButton> </MudButton>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>
</EditForm> </EditForm>
</div> </div>
</MudContainer>
@code { @code {

252
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -11,138 +11,140 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<MudTable Hover="true" Items="_schedule.Items.OrderBy(i => i.Index)" Dense="true" Class="mt-8" @bind-SelectedItem="_selectedItem"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<ToolBarContent> <MudTable Hover="true" Items="_schedule.Items.OrderBy(i => i.Index)" Dense="true" @bind-SelectedItem="_selectedItem">
<MudText Typo="Typo.h6">@_schedule.Name Items</MudText> <ToolBarContent>
</ToolBarContent> <MudText Typo="Typo.h6">@_schedule.Name Items</MudText>
<ColGroup> </ToolBarContent>
<col/> <ColGroup>
<col/> <col/>
<col/> <col/>
<col style="width: 60px;"/> <col/>
<col style="width: 60px;"/> <col style="width: 60px;"/>
<col style="width: 60px;"/> <col style="width: 60px;"/>
</ColGroup> <col style="width: 60px;"/>
<HeaderContent> </ColGroup>
<MudTh>Start Time</MudTh> <HeaderContent>
<MudTh>Collection</MudTh> <MudTh>Start Time</MudTh>
<MudTh>Playout Mode</MudTh> <MudTh>Collection</MudTh>
<MudTh/> <MudTh>Playout Mode</MudTh>
<MudTh/> <MudTh/>
<MudTh/> <MudTh/>
</HeaderContent> <MudTh/>
<RowTemplate> </HeaderContent>
<MudTd DataLabel="Start Time"> <RowTemplate>
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)"> <MudTd DataLabel="Start Time">
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") <MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
</MudText> @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
</MudTd> </MudText>
<MudTd DataLabel="Collection"> </MudTd>
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)"> <MudTd DataLabel="Collection">
@context.CollectionName <MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
</MudText> @context.CollectionName
</MudTd> </MudText>
<MudTd DataLabel="Playout Mode"> </MudTd>
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)"> <MudTd DataLabel="Playout Mode">
@context.PlayoutMode <MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue) @context.PlayoutMode
{ @if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue)
@($" ({context.MultipleCount})") {
} @($" ({context.MultipleCount})")
</MudText> }
</MudTd> </MudText>
<MudTd> </MudTd>
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward" <MudTd>
OnClick="@(_ => MoveItemUp(context))" <MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
Disabled="@(_schedule.Items.All(x => x.Index >= context.Index))"> OnClick="@(_ => MoveItemUp(context))"
</MudIconButton> Disabled="@(_schedule.Items.All(x => x.Index >= context.Index))">
</MudTd> </MudIconButton>
<MudTd> </MudTd>
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward" <MudTd>
OnClick="@(_ => MoveItemDown(context))" <MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
Disabled="@(_schedule.Items.All(x => x.Index <= context.Index))"> OnClick="@(_ => MoveItemDown(context))"
</MudIconButton> Disabled="@(_schedule.Items.All(x => x.Index <= context.Index))">
</MudTd> </MudIconButton>
<MudTd> </MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" <MudTd>
OnClick="@(_ => RemoveScheduleItem(context))"> <MudIconButton Icon="@Icons.Material.Filled.Delete"
</MudIconButton> OnClick="@(_ => RemoveScheduleItem(context))">
</MudTd> </MudIconButton>
</RowTemplate> </MudTd>
<PagerContent> </RowTemplate>
<MudTablePager/> <PagerContent>
</PagerContent> <MudTablePager/>
</MudTable> </PagerContent>
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddScheduleItem())" Class="mt-4"> </MudTable>
Add Schedule Item <MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddScheduleItem())" Class="mt-4">
</MudButton> Add Schedule Item
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4"> </MudButton>
Save Changes <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
</MudButton> Save Changes
</MudButton>
@if (_selectedItem is not null) @if (_selectedItem is not null)
{ {
<div style="max-width: 400px;"> <div style="max-width: 400px;">
<EditForm Model="_selectedItem"> <EditForm Model="_selectedItem">
<FluentValidator/> <FluentValidator/>
<MudCard Class="mt-6"> <MudCard Class="mt-6">
<MudCardContent> <MudCardContent>
<MudSelect Label="Start Type" @bind-Value="_selectedItem.StartType" For="@(() => _selectedItem.StartType)"> <MudSelect Label="Start Type" @bind-Value="_selectedItem.StartType" For="@(() => _selectedItem.StartType)">
@foreach (StartType startType in Enum.GetValues<StartType>()) @foreach (StartType startType in Enum.GetValues<StartType>())
{
<MudSelectItem Value="@startType">@startType</MudSelectItem>
}
</MudSelect>
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
<MudSelect Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
}
</MudSelect>
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection)
{ {
<MudSelectItem Value="@startType">@startType</MudSelectItem> <MudAutocomplete Class="mt-3"
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_selectedItem.Collection"
SearchFunc="@SearchMediaCollections"
ToStringFunc="@(c => c?.Name)"/>
} }
</MudSelect> @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
<MudSelect Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{ {
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem> <MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_selectedItem.MediaItem"
SearchFunc="@SearchTelevisionShows"
ToStringFunc="@(s => s?.Name)"/>
} }
</MudSelect> @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudAutocomplete Class="mt-3"
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_selectedItem.Collection"
SearchFunc="@SearchMediaCollections"
ToStringFunc="@(c => c?.Name)"/>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_selectedItem.MediaItem"
SearchFunc="@SearchTelevisionShows"
ToStringFunc="@(s => s?.Name)"/>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_selectedItem.MediaItem"
SearchFunc="@SearchTelevisionSeasons"
ToStringFunc="@(s => s?.Name)"/>
}
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
@foreach (PlayoutMode playoutMode in Enum.GetValues<PlayoutMode>())
{ {
<MudSelectItem Value="@playoutMode">@playoutMode</MudSelectItem> <MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_selectedItem.MediaItem"
SearchFunc="@SearchTelevisionSeasons"
ToStringFunc="@(s => s?.Name)"/>
} }
</MudSelect> <MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
<MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/> @foreach (PlayoutMode playoutMode in Enum.GetValues<PlayoutMode>())
<MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/> {
<MudElement HtmlTag="div" Class="mt-3"> <MudSelectItem Value="@playoutMode">@playoutMode</MudSelectItem>
<MudSwitch Label="Offline Tail" @bind-Checked="@_selectedItem.OfflineTail" For="@(() => _selectedItem.OfflineTail)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/> }
</MudElement> </MudSelect>
</MudCardContent> <MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/>
</MudCard> <MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
</EditForm> <MudElement HtmlTag="div" Class="mt-3">
</div> <MudSwitch Label="Offline Tail" @bind-Checked="@_selectedItem.OfflineTail" For="@(() => _selectedItem.OfflineTail)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
} </MudElement>
</MudCardContent>
</MudCard>
</EditForm>
</div>
}
</MudContainer>
@code { @code {

49
ErsatzTV/Pages/Schedules.razor

@ -5,7 +5,7 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_schedules" Dense="true" SelectedItemChanged="@(async (ProgramScheduleViewModel x) => await ScheduleSelected(x))"> <MudTable Hover="true" Items="_schedules" Dense="true" SelectedItemChanged="@(async (ProgramScheduleViewModel x) => await ScheduleSelected(x))">
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">Schedules</MudText> <MudText Typo="Typo.h6">Schedules</MudText>
@ -50,31 +50,32 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/schedules/add" Class="mt-4"> <MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/schedules/add" Class="mt-4">
Add Schedule Add Schedule
</MudButton> </MudButton>
@if (_selectedScheduleItems != null)
{
<MudTable Hover="true" Items="_selectedScheduleItems.OrderBy(i => i.Index)" Class="mt-8">
<ToolBarContent>
<MudText Typo="Typo.h6">@_selectedSchedule.Name Items</MudText>
</ToolBarContent>
<HeaderContent>
<MudTh>Start Time</MudTh>
<MudTh>Collection</MudTh>
<MudTh>Playout Mode</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start Time">
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
</MudTd>
<MudTd DataLabel="Collection">@context.Name</MudTd>
<MudTd DataLabel="Playout Mode">@context.PlayoutMode</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
}
</MudContainer> </MudContainer>
@if (_selectedScheduleItems != null)
{
<MudTable Hover="true" Items="_selectedScheduleItems.OrderBy(i => i.Index)" Class="mt-8">
<ToolBarContent>
<MudText Typo="Typo.h6">@_selectedSchedule.Name Items</MudText>
</ToolBarContent>
<HeaderContent>
<MudTh>Start Time</MudTh>
<MudTh>Collection</MudTh>
<MudTh>Playout Mode</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start Time">
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
</MudTd>
<MudTd DataLabel="Collection">@context.Name</MudTd>
<MudTd DataLabel="Playout Mode">@context.PlayoutMode</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
}
@code { @code {
private List<ProgramScheduleViewModel> _schedules; private List<ProgramScheduleViewModel> _schedules;

2
ErsatzTV/Pages/Search.razor

@ -13,7 +13,7 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div class="mb-6" style="display: flex; flex-direction: row;"> <div class="mb-6" style="display: flex; flex-direction: row;">
<MudText GutterBottom="true" Typo="Typo.h4">Search Results: "@_query"</MudText> <MudText GutterBottom="true" Typo="Typo.h4">Search Results: "@_query"</MudText>
</div> </div>

54
ErsatzTV/Pages/TelevisionEpisode.razor

@ -7,33 +7,35 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs> <MudContainer MaxWidth="MaxWidth.Large">
<MudCard Class="mb-6"> <MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<div style="display: flex; flex-direction: row;"> <MudCard Class="mb-6">
@if (!string.IsNullOrWhiteSpace(_episode.Poster)) <div style="display: flex; flex-direction: row;">
{ @if (!string.IsNullOrWhiteSpace(_episode.Poster))
<MudPaper style="display: flex; flex-direction: column"> {
<MudCardMedia Image="@($"/artwork/thumbnails/{_episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/> <MudPaper style="display: flex; flex-direction: column">
</MudPaper> <MudCardMedia Image="@($"/artwork/thumbnails/{_episode.Poster}")" Style="flex-grow: 1; height: 220px; width: 392px;"/>
} </MudPaper>
<MudCardContent Class="ml-3"> }
<div style="display: flex; flex-direction: column; height: 100%"> <MudCardContent Class="ml-3">
<MudText Typo="Typo.h4">@_episode.Title</MudText> <div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Plot</MudText> <MudText Typo="Typo.h4">@_episode.Title</MudText>
<MudText Style="flex-grow: 1">@_episode.Plot</MudText> <MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Plot</MudText>
<div class="mt-6"> <MudText Style="flex-grow: 1">@_episode.Plot</MudText>
<MudButton Variant="Variant.Filled" <div class="mt-6">
Color="Color.Primary" <MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add" Color="Color.Primary"
OnClick="@AddToCollection"> StartIcon="@Icons.Material.Filled.Add"
Add To Collection OnClick="@AddToCollection">
</MudButton> Add To Collection
</MudButton>
</div>
</div> </div>
</div> </MudCardContent>
</MudCardContent> </div>
</div> </MudCard>
</MudCard> </MudContainer>
</MudContainer> </MudContainer>
@code { @code {

92
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -15,53 +15,55 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs> <MudContainer MaxWidth="MaxWidth.Large">
<MudCard Class="mb-6"> <MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<div style="display: flex; flex-direction: row;"> <MudCard Class="mb-6">
@if (!string.IsNullOrWhiteSpace(_season.Poster)) <div style="display: flex; flex-direction: row;">
{ @if (!string.IsNullOrWhiteSpace(_season.Poster))
<MudPaper Style="flex-shrink: 0;"> {
<MudCardMedia Image="@($"/artwork/posters/{_season.Poster}")" Style="height: 440px; width: 304px;"/> <MudPaper Style="flex-shrink: 0;">
</MudPaper> <MudCardMedia Image="@($"/artwork/posters/{_season.Poster}")" Style="height: 440px; width: 304px;"/>
} </MudPaper>
<MudCardContent Class="mx-3 my-3"> }
<div style="display: flex; flex-direction: column; height: 100%"> <MudCardContent Class="mx-3 my-3">
<MudText Typo="Typo.h3">@_season.Title</MudText> <div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Year</MudText> <MudText Typo="Typo.h3">@_season.Title</MudText>
<MudText Style="flex-grow: 1">@_season.Plot</MudText> <MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Year</MudText>
<div class="mt-6"> <MudText Style="flex-grow: 1">@_season.Plot</MudText>
<MudButton Variant="Variant.Filled" <div class="mt-6">
Color="Color.Primary" <MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add" Color="Color.Primary"
OnClick="@AddToCollection"> StartIcon="@Icons.Material.Filled.Add"
Add To Collection OnClick="@AddToCollection">
</MudButton> Add To Collection
<MudButton Class="ml-3" </MudButton>
Variant="Variant.Filled" <MudButton Class="ml-3"
Color="Color.Primary" Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Schedule" Color="Color.Primary"
OnClick="@AddToSchedule"> StartIcon="@Icons.Material.Filled.Schedule"
Add To Schedule OnClick="@AddToSchedule">
</MudButton> Add To Schedule
</MudButton>
</div>
</div> </div>
</div> </MudCardContent>
</MudCardContent> </div>
</div> </MudCard>
</MudCard> </MudContainer>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid"> <MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
@foreach (TelevisionEpisodeCardViewModel card in _data.Cards) @foreach (TelevisionEpisodeCardViewModel card in _data.Cards)
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
Placeholder="@card.Placeholder" Placeholder="@card.Placeholder"
Link="@($"/media/tv/episodes/{card.EpisodeId}")" Link="@($"/media/tv/episodes/{card.EpisodeId}")"
AddToCollectionClicked="@AddEpisodeToCollection" AddToCollectionClicked="@AddEpisodeToCollection"
ContainerClass="media-card-episode-container mx-2" ContainerClass="media-card-episode-container mx-2"
CardClass="media-card-episode" CardClass="media-card-episode"
ArtworkKind="@ArtworkKind.Thumbnail"/> ArtworkKind="@ArtworkKind.Thumbnail"/>
} }
</MudContainer>
</MudContainer> </MudContainer>
@code { @code {

84
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -15,49 +15,51 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs> <MudContainer MaxWidth="MaxWidth.Large">
<MudCard Class="mb-6"> <MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<div style="display: flex; flex-direction: row;"> <MudCard Class="mb-6">
@if (!string.IsNullOrWhiteSpace(_show.Poster)) <div style="display: flex; flex-direction: row;">
{ @if (!string.IsNullOrWhiteSpace(_show.Poster))
<MudPaper Style="flex-shrink: 0;"> {
<MudCardMedia Image="@($"/artwork/posters/{_show.Poster}")" Style="height: 440px; width: 304px;"/> <MudPaper Style="flex-shrink: 0;">
</MudPaper> <MudCardMedia Image="@($"/artwork/posters/{_show.Poster}")" Style="height: 440px; width: 304px;"/>
} </MudPaper>
<MudCardContent Class="mx-3 my-3"> }
<div style="display: flex; flex-direction: column; height: 100%"> <MudCardContent Class="mx-3 my-3">
<MudText Typo="Typo.h3">@_show.Title</MudText> <div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_show.Year</MudText> <MudText Typo="Typo.h3">@_show.Title</MudText>
<MudText Style="flex-grow: 1">@_show.Plot</MudText> <MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_show.Year</MudText>
<div> <MudText Style="flex-grow: 1">@_show.Plot</MudText>
<MudButton Variant="Variant.Filled" <div>
Color="Color.Primary" <MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add" Color="Color.Primary"
OnClick="@AddToCollection"> StartIcon="@Icons.Material.Filled.Add"
Add To Collection OnClick="@AddToCollection">
</MudButton> Add To Collection
<MudButton Class="ml-3" </MudButton>
Variant="Variant.Filled" <MudButton Class="ml-3"
Color="Color.Primary" Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Schedule" Color="Color.Primary"
OnClick="@AddToSchedule"> StartIcon="@Icons.Material.Filled.Schedule"
Add To Schedule OnClick="@AddToSchedule">
</MudButton> Add To Schedule
</MudButton>
</div>
</div> </div>
</div> </MudCardContent>
</MudCardContent> </div>
</div> </MudCard>
</MudCard> </MudContainer>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid"> <MudContainer MaxWidth="MaxWidth.Large" Class="media-card-grid">
@foreach (TelevisionSeasonCardViewModel card in _data.Cards) @foreach (TelevisionSeasonCardViewModel card in _data.Cards)
{ {
<MediaCard Data="@card" Placeholder="@card.Placeholder" <MediaCard Data="@card" Placeholder="@card.Placeholder"
Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")" Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")"
AddToCollectionClicked="@AddSeasonToCollection"/> AddToCollectionClicked="@AddSeasonToCollection"/>
} }
</MudContainer>
</MudContainer> </MudContainer>
@code { @code {

46
ErsatzTV/Pages/TelevisionShowList.razor

@ -10,29 +10,31 @@
@inject IDialogService Dialog @inject IDialogService Dialog
@inject ChannelWriter<IBackgroundServiceRequest> Channel @inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudPaper Style="align-items: center; display: flex; justify-content: center;"> <MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px">
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft" <MudPaper Style="align-items: center; display: flex; justify-content: center;">
OnClick="@(() => PrevPage())" <MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
Disabled="@(_pageNumber <= 1)"> OnClick="@(() => PrevPage())"
</MudIconButton> Disabled="@(_pageNumber <= 1)">
<MudText Style="flex-grow: 1" </MudIconButton>
Align="Align.Center"> <MudText Style="flex-grow: 1"
@Math.Min((_pageNumber - 1) * _pageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * _pageSize) of @_data.Count Align="Align.Center">
</MudText> @Math.Min((_pageNumber - 1) * _pageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * _pageSize) of @_data.Count
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight" </MudText>
OnClick="@(() => NextPage())" Disabled="@(_pageNumber * _pageSize >= _data.Count)"> <MudIconButton Icon="@Icons.Material.Outlined.ChevronRight"
</MudIconButton> OnClick="@(() => NextPage())" Disabled="@(_pageNumber * _pageSize >= _data.Count)">
</MudPaper> </MudIconButton>
</MudContainer> </MudPaper>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> <MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionShowCardViewModel card in _data.Cards) @foreach (TelevisionShowCardViewModel card in _data.Cards)
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
Link="@($"/media/tv/shows/{card.TelevisionShowId}")" Link="@($"/media/tv/shows/{card.TelevisionShowId}")"
AddToCollectionClicked="@AddToCollection"/> AddToCollectionClicked="@AddToCollection"/>
} }
</MudContainer>
</MudContainer> </MudContainer>
@code { @code {

43
ErsatzTV/Shared/MainLayout.razor

@ -9,30 +9,30 @@
<MudLayout> <MudLayout>
<MudAppBar Elevation="1"> <MudAppBar Elevation="1">
<div style="min-width: 240px">
<img src="/images/ersatztv.png" alt="ErsatzTV"/>
</div>
<MudTextField T="string" <MudTextField T="string"
@ref="_textField" @ref=" _textField"
Placeholder="Search" AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Class="search-bar" Class="search-bar"
OnKeyDown="@OnSearchKeyDown" OnKeyDown="@OnSearchKeyDown"
Immediate="true"> Immediate="true">
</MudTextField> </MudTextField>
<MudAppBarSpacer/> <MudAppBarSpacer/>
<MudLink Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" Href="/iptv/channels.m3u" Target="_blank" Underline="Underline.None">M3U</MudLink> <MudLink Color="Color.Info" Href="/iptv/channels.m3u" Target="_blank" Underline="Underline.None">M3U</MudLink>
<MudLink Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" Href="/iptv/xmltv.xml" Target="_blank" Class="mx-4" Underline="Underline.None">XMLTV</MudLink> <MudLink Color="Color.Info" Href="/iptv/xmltv.xml" Target="_blank" Class="mx-4" Underline="Underline.None">XMLTV</MudLink>
<MudLink Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" Href="/swagger" Target="_blank" Class="mr-4" Underline="Underline.None">API</MudLink> <MudLink Color="Color.Info" Href="/swagger" Target="_blank" Class="mr-4" Underline="Underline.None">API</MudLink>
<MudDivider Vertical="true" FlexItem="true" DividerType="DividerType.Middle" Class="mx-4 my-5"/>
<MudTooltip Text="Discord"> <MudTooltip Text="Discord">
<MudIconButton Icon="fab fa-discord" Color="Color.Inherit" Link="https://discord.gg/hHaJm3yGy6" Target="_blank"/> <MudIconButton Icon="fab fa-discord" Color="Color.Primary" Link="https://discord.gg/hHaJm3yGy6" Target="_blank"/>
</MudTooltip> </MudTooltip>
<MudTooltip Text="GitHub"> <MudTooltip Text="GitHub">
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Link="https://github.com/jasongdove/ErsatzTV" Target="_blank"/> <MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Primary" Link="https://github.com/jasongdove/ErsatzTV" Target="_blank"/>
</MudTooltip> </MudTooltip>
</MudAppBar> </MudAppBar>
<MudDrawer Open="true" Elevation="2"> <MudDrawer Open="true" Elevation="2" ClipMode="DrawerClipMode.Always">
<MudDrawerHeader>
<MudText Typo="Typo.h6">ErsatzTV</MudText>
</MudDrawerHeader>
<MudNavMenu> <MudNavMenu>
<MudNavLink Href="/" Match="NavLinkMatch.All">Home</MudNavLink> <MudNavLink Href="/" Match="NavLinkMatch.All">Home</MudNavLink>
<MudNavLink Href="/channels">Channels</MudNavLink> <MudNavLink Href="/channels">Channels</MudNavLink>
@ -57,9 +57,7 @@
</MudNavMenu> </MudNavMenu>
</MudDrawer> </MudDrawer>
<MudMainContent> <MudMainContent>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> @Body
@Body
</MudContainer>
</MudMainContent> </MudMainContent>
</MudLayout> </MudLayout>
@ -78,9 +76,18 @@
{ {
Palette = new Palette Palette = new Palette
{ {
DrawerBackground = current.Palette.Background, ActionDefault = "rgba(255,255,255, 0.80)",
Background = current.Palette.BackgroundGrey, Primary = "#009000",
Tertiary = Colors.Shades.White AppbarBackground = "#121212",
Background = "#272727",
DrawerBackground = "#1f1f1f",
Surface = "#1f1f1f",
DrawerText = "rgba(255,255,255, 0.80)",
TextPrimary = "rgba(255,255,255, 0.80)",
TextSecondary = "rgba(255,255,255, 0.80)",
Info = "#00c0c0",
Tertiary = "#00c000",
White = Colors.Shades.White
} }
}; };
} }

4
ErsatzTV/Shared/MediaCard.razor

@ -9,7 +9,7 @@
<MudPaper Class="@($"media-card {CardClass}")" Style="@ArtworkForItem()"> <MudPaper Class="@($"media-card {CardClass}")" Style="@ArtworkForItem()">
@if (string.IsNullOrWhiteSpace(Data.Poster)) @if (string.IsNullOrWhiteSpace(Data.Poster))
{ {
<MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-disabled"> <MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-primary">
@GetPlaceholder(Data.SortTitle) @GetPlaceholder(Data.SortTitle)
</MudText> </MudText>
} }
@ -39,7 +39,7 @@
<MudPaper Class="@($"media-card {CardClass}")" Style="@ArtworkForItem()"> <MudPaper Class="@($"media-card {CardClass}")" Style="@ArtworkForItem()">
@if (string.IsNullOrWhiteSpace(Data.Poster)) @if (string.IsNullOrWhiteSpace(Data.Poster))
{ {
<MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-disabled"> <MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-primary">
@GetPlaceholder(Data.SortTitle) @GetPlaceholder(Data.SortTitle)
</MudText> </MudText>
} }

38
ErsatzTV/wwwroot/css/site.css

@ -1,4 +1,6 @@
.media-card-grid { .mud-breadcrumb-separator > span { color: inherit !important; }
.media-card-grid {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
@ -68,4 +70,38 @@
.search-bar .mud-input.mud-input-outlined .mud-input-outlined-border { .search-bar .mud-input.mud-input-outlined .mud-input-outlined-border {
border: none; border: none;
border-radius: 4px; border-radius: 4px;
}
.fanart-container {
position: relative;
width: 100%;
z-index: -1;
}
.fanart-container img {
-o-object-fit: cover;
height: 400px;
object-fit: cover;
position: absolute;
transition: opacity 5s ease;
width: 100%;
}
.fanart-container > .fanart-tint {
background: linear-gradient(360deg, black, transparent);
height: 400px;
opacity: 0.85;
position: absolute;
width: 100%;
z-index: 1;
}
.media-item-title {
color: #fff;
text-shadow: 1px 1px 5px #000;
}
.media-item-subtitle {
color: #fff;
text-shadow: 1px 1px 5px #000;
} }

BIN
ErsatzTV/wwwroot/images/ersatztv.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Loading…
Cancel
Save