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/57/head v0.0.14-prealpha
Jason Dove 4 years ago committed by GitHub
parent
commit
e3b91e62ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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 @@ -42,6 +42,7 @@ namespace ErsatzTV.Application.Images.Queries
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder
};

8
ErsatzTV.Application/Movies/Mapper.cs

@ -13,8 +13,12 @@ namespace ErsatzTV.Application.Movies @@ -13,8 +13,12 @@ namespace ErsatzTV.Application.Movies
metadata.Title,
metadata.Year?.ToString(),
metadata.Plot,
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster))
.Match(a => a.Path, string.Empty));
Artwork(metadata, ArtworkKind.Poster),
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 @@ @@ -1,4 +1,4 @@
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 @@ @@ -4,6 +4,7 @@
{
Poster = 0,
Thumbnail = 1,
Logo = 2
Logo = 2,
FanArt = 3
}
}

1
ErsatzTV.Core/FileSystemLayout.cs

@ -24,5 +24,6 @@ namespace ErsatzTV.Core @@ -24,5 +24,6 @@ namespace ErsatzTV.Core
public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");
public static readonly string ThumbnailCacheFolder = Path.Combine(ArtworkCacheFolder, "thumbnails");
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 @@ -97,10 +97,10 @@ namespace ErsatzTV.Core.Metadata
metadata.Artwork ??= new List<Artwork>();
Option<Artwork> maybePoster =
Option<Artwork> maybeArtwork =
Optional(metadata.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == artworkKind);
bool shouldRefresh = maybePoster.Match(
bool shouldRefresh = maybeArtwork.Match(
artwork => artwork.DateUpdated < lastWriteTime,
true);
@ -109,7 +109,7 @@ namespace ErsatzTV.Core.Metadata @@ -109,7 +109,7 @@ namespace ErsatzTV.Core.Metadata
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
maybePoster.Match(
maybeArtwork.Match(
artwork =>
{
artwork.Path = cacheName;

38
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -80,7 +80,8 @@ namespace ErsatzTV.Core.Metadata @@ -80,7 +80,8 @@ namespace ErsatzTV.Core.Metadata
.GetOrAdd(libraryPath, file)
.BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie))
.BindT(UpdateMetadata)
.BindT(UpdatePoster);
.BindT(UpdatePoster)
.BindT(UpdateFanArt);
maybeMovie.IfLeft(
error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value));
@ -139,7 +140,7 @@ namespace ErsatzTV.Core.Metadata @@ -139,7 +140,7 @@ namespace ErsatzTV.Core.Metadata
{
try
{
await LocatePoster(movie).IfSomeAsync(
await LocateArtwork(movie, ArtworkKind.Poster).IfSomeAsync(
async posterFile =>
{
MovieMetadata metadata = movie.MovieMetadata.Head();
@ -157,6 +158,28 @@ namespace ErsatzTV.Core.Metadata @@ -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)
{
string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
@ -167,12 +190,19 @@ namespace ErsatzTV.Core.Metadata @@ -167,12 +190,19 @@ namespace ErsatzTV.Core.Metadata
.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 folder = Path.GetDirectoryName(path) ?? string.Empty;
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));
Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone();
return result;

2
ErsatzTV.Infrastructure/Images/ImageCache.cs

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

1
ErsatzTV.sln.DotSettings

@ -14,6 +14,7 @@ @@ -14,6 +14,7 @@
<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/=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/=featurette/@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; @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers
{
[ResponseCache(Duration = 3600)]
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
public class PostersController : ControllerBase
@ -37,6 +38,16 @@ namespace ErsatzTV.Controllers @@ -37,6 +38,16 @@ namespace ErsatzTV.Controllers
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}")]
public async Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path)
{

94
ErsatzTV/Pages/ChannelEditor.razor

@ -10,55 +10,57 @@ @@ -10,55 +10,57 @@
@inject ISnackbar Snackbar
@inject IMediator Mediator
<div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Channel" : "Add Channel")</MudText>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Channel" : "Add Channel")</MudText>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/>
<MudCard>
<MudCardContent>
<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)"/>
<MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.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))
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/>
<MudCard>
<MudCardContent>
<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)"/>
<MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
@foreach (StreamingMode streamingMode in Enum.GetValues<StreamingMode>())
{
<MudElement HtmlTag="img" src="@($"iptv/logos/{_model.Logo}")" Style="max-height: 50px"/>
<MudSelectItem Value="@streamingMode">@streamingMode</MudSelectItem>
}
</MudItem>
<MudItem xs="6">
<MudButton Class="ml-auto" HtmlTag="label"
Variant="Variant.Filled"
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>
</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"/>
}
</MudItem>
<MudItem xs="6">
<MudButton Class="ml-auto" HtmlTag="label"
Variant="Variant.Filled"
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 {

24
ErsatzTV/Pages/Channels.razor

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

34
ErsatzTV/Pages/CollectionEditor.razor

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

128
ErsatzTV/Pages/CollectionItems.razor

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

30
ErsatzTV/Pages/Collections.razor

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

32
ErsatzTV/Pages/FFmpeg.razor

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

185
ErsatzTV/Pages/FFmpegEditor.razor

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

138
ErsatzTV/Pages/Index.razor

@ -1,70 +1,72 @@ @@ -1,70 +1,72 @@
@page "/"
<MudCard>
<MudCardContent>
<MudText Typo="Typo.h3">Welcome to ErsatzTV!</MudText>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Channels</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>
<MudText Class="mt-3">
In <b>TransportStream</b> mode, the channel will also require an <b>FFmpeg profile</b> to configure transcoding and normalization.
In <b>HttpLiveStreaming</b> mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">FFmpeg Profiles</MudText>
<MudText>
<MudLink Href="/ffmpeg">FFmpeg Profiles</MudLink> are collections of FFmpeg settings that are applied at the channel level.
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.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Libraries</MudText>
<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.
Support for Plex libraries is under active development; Jellyfin and Emby library support is planned.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Collections</MudText>
<MudText>
<MudLink Href="/media/collections">Collections</MudLink> have a <b>name</b> and contain a logical grouping of media items.
Collections may contain shows, seasons, episodes or movies.
Collections containing shows and seasons are automatically updated as media is added or removed from the linked shows and seasons.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Schedules</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>
<MudText Class="mt-3 mb-2">Three <b>collection playback orders</b> are supported:</MudText>
<ul class="mud-typography-body1">
<li><b>Random</b> - to randomly play collection items; repeating is allowed before all collection items have been played.</li>
<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>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>
</ul>
<MudText Class="mt-3">
Schedule items have a <b>start type</b>, a <b>start time</b>, a <b>collection</b> and a <b>playout mode</b>.
</MudText>
<MudText Class="mt-3">
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>
<MudText Class="mt-3 mb-2">Four <b>playout modes</b> are supported:</MudText>
<ul class="mud-typography-body1">
<li><b>One</b> - to play one media item from the collection before 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>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>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>
</ul>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Playouts</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.
</MudText>
</MudElement>
</MudCardContent>
</MudCard>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCard>
<MudCardContent>
<MudText Typo="Typo.h3">Welcome to ErsatzTV!</MudText>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Channels</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>
<MudText Class="mt-3">
In <b>TransportStream</b> mode, the channel will also require an <b>FFmpeg profile</b> to configure transcoding and normalization.
In <b>HttpLiveStreaming</b> mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">FFmpeg Profiles</MudText>
<MudText>
<MudLink Href="/ffmpeg">FFmpeg Profiles</MudLink> are collections of FFmpeg settings that are applied at the channel level.
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.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Libraries</MudText>
<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.
Support for Plex libraries is under active development; Jellyfin and Emby library support is planned.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Collections</MudText>
<MudText>
<MudLink Href="/media/collections">Collections</MudLink> have a <b>name</b> and contain a logical grouping of media items.
Collections may contain shows, seasons, episodes or movies.
Collections containing shows and seasons are automatically updated as media is added or removed from the linked shows and seasons.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Schedules</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>
<MudText Class="mt-3 mb-2">Three <b>collection playback orders</b> are supported:</MudText>
<ul class="mud-typography-body1">
<li><b>Random</b> - to randomly play collection items; repeating is allowed before all collection items have been played.</li>
<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>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>
</ul>
<MudText Class="mt-3">
Schedule items have a <b>start type</b>, a <b>start time</b>, a <b>collection</b> and a <b>playout mode</b>.
</MudText>
<MudText Class="mt-3">
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>
<MudText Class="mt-3 mb-2">Four <b>playout modes</b> are supported:</MudText>
<ul class="mud-typography-body1">
<li><b>One</b> - to play one media item from the collection before 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>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>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>
</ul>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Playouts</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.
</MudText>
</MudElement>
</MudCardContent>
</MudCard>
</MudContainer>

2
ErsatzTV/Pages/Libraries.razor

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

2
ErsatzTV/Pages/LocalLibraryEditor.razor

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

2
ErsatzTV/Pages/LocalLibraryPathEditor.razor

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
@inject IEntityLocker Locker
@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>
<div style="max-width: 400px;">
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">

2
ErsatzTV/Pages/Logs.razor

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

57
ErsatzTV/Pages/Movie.razor

@ -7,33 +7,42 @@ @@ -7,33 +7,42 @@
@inject IDialogService Dialog
@inject NavigationManager NavigationManager
<MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<div style="display: flex; flex-direction: row;">
@if (!string.IsNullOrWhiteSpace(_movie.Poster))
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
<div class="fanart-tint"></div>
@if (!string.IsNullOrWhiteSpace(_movie.FanArt))
{
<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;">
<MudCardMedia Image="@($"/artwork/posters/{_movie.Poster}")" Style="height: 440px; width: 304px;"/>
</MudPaper>
<MudCard Elevation="2" Class="mb-6">
<MudCardContent Class="mx-3 my-3" Style="height: 100%">
<MudText Style="flex-grow: 1">@_movie.Plot</MudText>
</MudCardContent>
</MudCard>
}
<MudCardContent Class="mx-3 my-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h3">@_movie.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_movie.Year</MudText>
<MudText Style="flex-grow: 1">@_movie.Plot</MudText>
<div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
</div>
</div>
</MudCardContent>
<div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
</div>
</div>
</MudCard>
</div>
</MudContainer>
@code {

46
ErsatzTV/Pages/MovieList.razor

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

36
ErsatzTV/Pages/PlayoutEditor.razor

@ -8,24 +8,26 @@ @@ -8,24 +8,26 @@
@inject ISnackbar Snackbar
@inject IMediator Mediator
<div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">Add Playout</MudText>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">Add Playout</MudText>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/>
<MudCard>
<MudCardContent>
<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)"/>
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
Add Playout
</MudButton>
</MudCardActions>
</MudCard>
</EditForm>
</div>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidator/>
<MudCard>
<MudCardContent>
<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)"/>
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
Add Playout
</MudButton>
</MudCardActions>
</MudCard>
</EditForm>
</div>
</MudContainer>
@code {

2
ErsatzTV/Pages/Playouts.razor

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
@inject IDialogService Dialog
@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))">
<ToolBarContent>
<MudText Typo="Typo.h6">Playouts</MudText>

46
ErsatzTV/Pages/ScheduleEditor.razor

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

252
ErsatzTV/Pages/ScheduleItemsEditor.razor

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

49
ErsatzTV/Pages/Schedules.razor

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
@inject IDialogService Dialog
@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))">
<ToolBarContent>
<MudText Typo="Typo.h6">Schedules</MudText>
@ -50,31 +50,32 @@ @@ -50,31 +50,32 @@
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="/schedules/add" Class="mt-4">
Add Schedule
</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>
@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 {
private List<ProgramScheduleViewModel> _schedules;

2
ErsatzTV/Pages/Search.razor

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
@inject IDialogService Dialog
@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;">
<MudText GutterBottom="true" Typo="Typo.h4">Search Results: "@_query"</MudText>
</div>

54
ErsatzTV/Pages/TelevisionEpisode.razor

@ -7,33 +7,35 @@ @@ -7,33 +7,35 @@
@inject IDialogService Dialog
@inject NavigationManager NavigationManager
<MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<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>
}
<MudCardContent Class="ml-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h4">@_episode.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Plot</MudText>
<MudText Style="flex-grow: 1">@_episode.Plot</MudText>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
<MudCard Class="mb-6">
<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>
}
<MudCardContent Class="ml-3">
<div style="display: flex; flex-direction: column; height: 100%">
<MudText Typo="Typo.h4">@_episode.Title</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-6 mud-text-secondary">@_season.Plot</MudText>
<MudText Style="flex-grow: 1">@_episode.Plot</MudText>
<div class="mt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@AddToCollection">
Add To Collection
</MudButton>
</div>
</div>
</div>
</MudCardContent>
</div>
</MudCard>
</MudCardContent>
</div>
</MudCard>
</MudContainer>
</MudContainer>
@code {

92
ErsatzTV/Pages/TelevisionEpisodeList.razor

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

84
ErsatzTV/Pages/TelevisionSeasonList.razor

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

46
ErsatzTV/Pages/TelevisionShowList.razor

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

43
ErsatzTV/Shared/MainLayout.razor

@ -9,30 +9,30 @@ @@ -9,30 +9,30 @@
<MudLayout>
<MudAppBar Elevation="1">
<div style="min-width: 240px">
<img src="/images/ersatztv.png" alt="ErsatzTV"/>
</div>
<MudTextField T="string"
@ref="_textField"
Placeholder="Search"
@ref=" _textField"
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Variant="Variant.Outlined"
Class="search-bar"
OnKeyDown="@OnSearchKeyDown"
Immediate="true">
</MudTextField>
<MudAppBarSpacer/>
<MudLink Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" 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 Style="@($"color:{Colors.Shades.White}")" Color="Color.Inherit" 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"/>
<MudLink Color="Color.Info" Href="/iptv/channels.m3u" Target="_blank" Underline="Underline.None">M3U</MudLink>
<MudLink Color="Color.Info" Href="/iptv/xmltv.xml" Target="_blank" Class="mx-4" Underline="Underline.None">XMLTV</MudLink>
<MudLink Color="Color.Info" Href="/swagger" Target="_blank" Class="mr-4" Underline="Underline.None">API</MudLink>
<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 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>
</MudAppBar>
<MudDrawer Open="true" Elevation="2">
<MudDrawerHeader>
<MudText Typo="Typo.h6">ErsatzTV</MudText>
</MudDrawerHeader>
<MudDrawer Open="true" Elevation="2" ClipMode="DrawerClipMode.Always">
<MudNavMenu>
<MudNavLink Href="/" Match="NavLinkMatch.All">Home</MudNavLink>
<MudNavLink Href="/channels">Channels</MudNavLink>
@ -57,9 +57,7 @@ @@ -57,9 +57,7 @@
</MudNavMenu>
</MudDrawer>
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
@Body
</MudContainer>
@Body
</MudMainContent>
</MudLayout>
@ -78,9 +76,18 @@ @@ -78,9 +76,18 @@
{
Palette = new Palette
{
DrawerBackground = current.Palette.Background,
Background = current.Palette.BackgroundGrey,
Tertiary = Colors.Shades.White
ActionDefault = "rgba(255,255,255, 0.80)",
Primary = "#009000",
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 @@ @@ -9,7 +9,7 @@
<MudPaper Class="@($"media-card {CardClass}")" Style="@ArtworkForItem()">
@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)
</MudText>
}
@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
<MudPaper Class="@($"media-card {CardClass}")" Style="@ArtworkForItem()">
@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)
</MudText>
}

38
ErsatzTV/wwwroot/css/site.css

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
.media-card-grid {
.mud-breadcrumb-separator > span { color: inherit !important; }
.media-card-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
@ -68,4 +70,38 @@ @@ -68,4 +70,38 @@
.search-bar .mud-input.mud-input-outlined .mud-input-outlined-border {
border: none;
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