Merge pull request #171 from mateuszskoczek/features/genres
genre page added
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WatchIt.Common.Model.Genres;
|
||||
using WatchIt.Common.Model.Media;
|
||||
using WatchIt.WebAPI.Services.Controllers.Genres;
|
||||
|
||||
namespace WatchIt.WebAPI.Controllers;
|
||||
@@ -10,16 +11,20 @@ namespace WatchIt.WebAPI.Controllers;
|
||||
[Route("genres")]
|
||||
public class GenresController(IGenresControllerService genresControllerService) : ControllerBase
|
||||
{
|
||||
#region METHODS
|
||||
|
||||
#region Main
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(IEnumerable<GenreResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> GetAll(GenreQueryParameters query) => await genresControllerService.GetAll(query);
|
||||
public async Task<ActionResult> GetGenres(GenreQueryParameters query) => await genresControllerService.GetGenres(query);
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(GenreResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Get([FromRoute]short id) => await genresControllerService.Get(id);
|
||||
public async Task<ActionResult> GetGenre([FromRoute]short id) => await genresControllerService.GetGenre(id);
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
@@ -27,7 +32,7 @@ public class GenresController(IGenresControllerService genresControllerService)
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> Post([FromBody]GenreRequest body) => await genresControllerService.Post(body);
|
||||
public async Task<ActionResult> PostGenre([FromBody]GenreRequest body) => await genresControllerService.PostGenre(body);
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[Authorize]
|
||||
@@ -35,7 +40,7 @@ public class GenresController(IGenresControllerService genresControllerService)
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Put([FromRoute]short id, [FromBody]GenreRequest body) => await genresControllerService.Put(id, body);
|
||||
public async Task<ActionResult> PutGenre([FromRoute]short id, [FromBody]GenreRequest body) => await genresControllerService.PutGenre(id, body);
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize]
|
||||
@@ -44,5 +49,19 @@ public class GenresController(IGenresControllerService genresControllerService)
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Delete([FromRoute]short id) => await genresControllerService.Delete(id);
|
||||
public async Task<ActionResult> DeleteGenre([FromRoute]short id) => await genresControllerService.DeleteGenre(id);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Media
|
||||
|
||||
[HttpGet("{id}/media")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(IEnumerable<MediaResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> GetGenreMedia([FromRoute]short id, MediaQueryParameters query) => await genresControllerService.GetGenreMedia(id, query);
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WatchIt.Common.Model.Genres;
|
||||
using WatchIt.Common.Model.Media;
|
||||
using WatchIt.Database;
|
||||
using WatchIt.Database.Model.Media;
|
||||
using WatchIt.WebAPI.Services.Controllers.Common;
|
||||
@@ -12,14 +13,16 @@ public class GenresControllerService(DatabaseContext database, IUserService user
|
||||
{
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async Task<RequestResult> GetAll(GenreQueryParameters query)
|
||||
#region Main
|
||||
|
||||
public async Task<RequestResult> GetGenres(GenreQueryParameters query)
|
||||
{
|
||||
IEnumerable<GenreResponse> data = await database.Genres.Select(x => new GenreResponse(x)).ToListAsync();
|
||||
data = query.PrepareData(data);
|
||||
return RequestResult.Ok(data);
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Get(short id)
|
||||
public async Task<RequestResult> GetGenre(short id)
|
||||
{
|
||||
Genre? item = await database.Genres.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (item is null)
|
||||
@@ -31,7 +34,7 @@ public class GenresControllerService(DatabaseContext database, IUserService user
|
||||
return RequestResult.Ok(data);
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Post(GenreRequest data)
|
||||
public async Task<RequestResult> PostGenre(GenreRequest data)
|
||||
{
|
||||
UserValidator validator = userService.GetValidator().MustBeAdmin();
|
||||
if (!validator.IsValid)
|
||||
@@ -46,7 +49,7 @@ public class GenresControllerService(DatabaseContext database, IUserService user
|
||||
return RequestResult.Created($"genres/{item.Id}", new GenreResponse(item));
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Put(short id, GenreRequest data)
|
||||
public async Task<RequestResult> PutGenre(short id, GenreRequest data)
|
||||
{
|
||||
UserValidator validator = userService.GetValidator().MustBeAdmin();
|
||||
if (!validator.IsValid)
|
||||
@@ -66,7 +69,7 @@ public class GenresControllerService(DatabaseContext database, IUserService user
|
||||
return RequestResult.Ok();
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Delete(short id)
|
||||
public async Task<RequestResult> DeleteGenre(short id)
|
||||
{
|
||||
UserValidator validator = userService.GetValidator().MustBeAdmin();
|
||||
if (!validator.IsValid)
|
||||
@@ -90,4 +93,24 @@ public class GenresControllerService(DatabaseContext database, IUserService user
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Media
|
||||
|
||||
public async Task<RequestResult> GetGenreMedia(short id, MediaQueryParameters query)
|
||||
{
|
||||
if (!database.Genres.Any(x => x.Id == id))
|
||||
{
|
||||
return RequestResult.NotFound();
|
||||
}
|
||||
|
||||
IEnumerable<Database.Model.Media.Media> rawData = await database.Media.Where(x => x.MediaGenres.Any(y => y.GenreId == id))
|
||||
.ToListAsync();
|
||||
IEnumerable<MediaResponse> data = rawData.Select(x => new MediaResponse(x, database.MediaMovies.Any(y => y.Id == x.Id) ? MediaType.Movie : MediaType.Series));
|
||||
data = query.PrepareData(data);
|
||||
return RequestResult.Ok(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
using WatchIt.Common.Model.Genres;
|
||||
using WatchIt.Common.Model.Media;
|
||||
using WatchIt.WebAPI.Services.Controllers.Common;
|
||||
|
||||
namespace WatchIt.WebAPI.Services.Controllers.Genres;
|
||||
|
||||
public interface IGenresControllerService
|
||||
{
|
||||
Task<RequestResult> GetAll(GenreQueryParameters query);
|
||||
Task<RequestResult> Get(short id);
|
||||
Task<RequestResult> Post(GenreRequest data);
|
||||
Task<RequestResult> Put(short id, GenreRequest data);
|
||||
Task<RequestResult> Delete(short id);
|
||||
Task<RequestResult> GetGenres(GenreQueryParameters query);
|
||||
Task<RequestResult> GetGenre(short id);
|
||||
Task<RequestResult> PostGenre(GenreRequest data);
|
||||
Task<RequestResult> PutGenre(short id, GenreRequest data);
|
||||
Task<RequestResult> DeleteGenre(short id);
|
||||
Task<RequestResult> GetGenreMedia(short id, MediaQueryParameters query);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using WatchIt.Common.Model.Genders;
|
||||
using WatchIt.Common.Model.Genres;
|
||||
using WatchIt.Common.Model.Media;
|
||||
using WatchIt.Common.Services.HttpClient;
|
||||
using WatchIt.Website.Services.Configuration;
|
||||
|
||||
namespace WatchIt.Website.Services.Client.Genres;
|
||||
|
||||
public class GenresClientService : BaseClientService, IGenresClientService
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
private IHttpClientService _httpClientService;
|
||||
private IConfigurationService _configurationService;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public GenresClientService(IHttpClientService httpClientService, IConfigurationService configurationService) : base(configurationService)
|
||||
{
|
||||
_httpClientService = httpClientService;
|
||||
_configurationService = configurationService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
#region Main
|
||||
|
||||
public async Task GetGenres(GenreQueryParameters? query = null, Action<IEnumerable<GenreResponse>>? successAction = null)
|
||||
{
|
||||
string url = GetUrl(EndpointsConfiguration.Genres.GetGenres);
|
||||
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
|
||||
request.Query = query;
|
||||
|
||||
HttpResponse response = await _httpClientService.SendRequestAsync(request);
|
||||
response.RegisterActionFor2XXSuccess(successAction)
|
||||
.ExecuteAction();
|
||||
}
|
||||
|
||||
public async Task GetGenre(long id, Action<GenreResponse>? successAction = null, Action? notFoundAction = null)
|
||||
{
|
||||
string url = GetUrl(EndpointsConfiguration.Genres.GetGenre, id);
|
||||
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
|
||||
|
||||
HttpResponse response = await _httpClientService.SendRequestAsync(request);
|
||||
response.RegisterActionFor2XXSuccess(successAction)
|
||||
.RegisterActionFor404NotFound(notFoundAction)
|
||||
.ExecuteAction();
|
||||
}
|
||||
|
||||
public async Task PostGenre(GenreRequest data, Action<GenreResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
|
||||
{
|
||||
string url = GetUrl(EndpointsConfiguration.Genres.PostGenre);
|
||||
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.Post, url);
|
||||
request.Body = data;
|
||||
|
||||
HttpResponse response = await _httpClientService.SendRequestAsync(request);
|
||||
response.RegisterActionFor2XXSuccess(successAction)
|
||||
.RegisterActionFor400BadRequest(badRequestAction)
|
||||
.RegisterActionFor401Unauthorized(unauthorizedAction)
|
||||
.RegisterActionFor403Forbidden(forbiddenAction)
|
||||
.ExecuteAction();
|
||||
}
|
||||
|
||||
public async Task DeleteGenre(long id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
|
||||
{
|
||||
string url = GetUrl(EndpointsConfiguration.Genres.DeleteGenre, id);
|
||||
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
|
||||
|
||||
HttpResponse response = await _httpClientService.SendRequestAsync(request);
|
||||
response.RegisterActionFor2XXSuccess(successAction)
|
||||
.RegisterActionFor401Unauthorized(unauthorizedAction)
|
||||
.RegisterActionFor403Forbidden(forbiddenAction)
|
||||
.ExecuteAction();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Media
|
||||
|
||||
public async Task GetGenreMedia(short id, MediaQueryParameters? query = null, Action<IEnumerable<MediaResponse>>? successAction = null, Action notFoundAction = null)
|
||||
{
|
||||
string url = GetUrl(EndpointsConfiguration.Genres.GetGenreMedia, id);
|
||||
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
|
||||
request.Query = query;
|
||||
|
||||
HttpResponse response = await _httpClientService.SendRequestAsync(request);
|
||||
response.RegisterActionFor2XXSuccess(successAction)
|
||||
.RegisterActionFor404NotFound(notFoundAction)
|
||||
.ExecuteAction();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override string GetServiceBase() => EndpointsConfiguration.Genres.Base;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using WatchIt.Common.Model.Genres;
|
||||
using WatchIt.Common.Model.Media;
|
||||
|
||||
namespace WatchIt.Website.Services.Client.Genres;
|
||||
|
||||
public interface IGenresClientService
|
||||
{
|
||||
Task GetGenres(GenreQueryParameters? query = null, Action<IEnumerable<GenreResponse>>? successAction = null);
|
||||
Task GetGenre(long id, Action<GenreResponse>? successAction = null, Action? notFoundAction = null);
|
||||
Task PostGenre(GenreRequest data, Action<GenreResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
|
||||
Task DeleteGenre(long id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
|
||||
Task GetGenreMedia(short id, MediaQueryParameters? query = null, Action<IEnumerable<MediaResponse>>? successAction = null, Action notFoundAction = null);
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
public class Genres
|
||||
{
|
||||
public string Base { get; set; }
|
||||
public string GetAll { get; set; }
|
||||
public string Get { get; set; }
|
||||
public string Post { get; set; }
|
||||
public string Put { get; set; }
|
||||
public string Delete { get; set; }
|
||||
public string GetGenres { get; set; }
|
||||
public string GetGenre { get; set; }
|
||||
public string PostGenre { get; set; }
|
||||
public string PutGenre { get; set; }
|
||||
public string DeleteGenre { get; set; }
|
||||
public string GetGenreMedia { get; set; }
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public partial class ListComponent<TItem, TQuery> : ComponentBase where TItem :
|
||||
[Parameter] public Func<long, RatingRequest, Task>? PutRatingMethod { get; set; }
|
||||
[Parameter] public Func<long, Task>? DeleteRatingMethod { get; set; }
|
||||
[Parameter] public required string PosterPlaceholder { get; set; }
|
||||
[Parameter] public TQuery Query { get; set; } = Activator.CreateInstance<TQuery>()!;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -54,14 +55,6 @@ public partial class ListComponent<TItem, TQuery> : ComponentBase where TItem :
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public TQuery Query { get; set; } = Activator.CreateInstance<TQuery>()!;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
@inherits WatchIt.Website.Components.Common.ListComponent.FilterFormComponent<WatchIt.Common.Model.Media.MediaResponse, WatchIt.Common.Model.Media.MediaQueryParameters>
|
||||
|
||||
|
||||
|
||||
<EditForm Model="@(Query)">
|
||||
<div class="container-grid">
|
||||
<div class="row mb-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Type</span>
|
||||
<InputSelect TValue="MediaType?" class="col form-control" @bind-Value="@(Query.Type)">
|
||||
<option @onclick="() => Query.Type = null">No choice</option>
|
||||
<option value="@(MediaType.Movie)">Movies</option>
|
||||
<option value="@(MediaType.Series)">TV series</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Title</span>
|
||||
<InputText class="col form-control" placeholder="Search with regex" @bind-Value="@(Query.Title)"></InputText>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Original title</span>
|
||||
<InputText class="col form-control" placeholder="Search with regex" @bind-Value="@(Query.OriginalTitle)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Description</span>
|
||||
<InputText class="col form-control" placeholder="Search with regex" @bind-Value="@(Query.Description)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Release date</span>
|
||||
<InputDate TValue="DateOnly?" class="col form-control" @bind-Value="@(Query.ReleaseDateFrom)"/>
|
||||
<span class="col-auto input-group-text">-</span>
|
||||
<InputDate TValue="DateOnly?" class="col form-control" @bind-Value="@(Query.ReleaseDateTo)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Length</span>
|
||||
<NumericEdit TValue="short?" Class="col form-control" Min="0" @bind-Value="@(Query.LengthFrom)"/>
|
||||
<span class="col-auto input-group-text">-</span>
|
||||
<NumericEdit TValue="short?" Class="col form-control" Min="0" @bind-Value="@(Query.LengthTo)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Rating (count)</span>
|
||||
<NumericEdit TValue="long?" Class="col form-control" Min="0" @bind-Value="@(Query.RatingCountFrom)"/>
|
||||
<span class="col-auto input-group-text">-</span>
|
||||
<NumericEdit TValue="long?" Class="col form-control" Min="0" @bind-Value="@(Query.RatingCountTo)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-1">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="col-3 input-group-text">Rating (average)</span>
|
||||
<NumericEdit TValue="decimal?" Class="col form-control" Min="0" Max="10" Step="@(0.01M)" @bind-Value="@(Query.RatingAverageFrom)"/>
|
||||
<span class="col-auto input-group-text">-</span>
|
||||
<NumericEdit TValue="decimal?" Class="col form-control" Min="0" Max="10" Step="@(0.01M)" @bind-Value="@(Query.RatingAverageTo)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="panel" role="button" @onclick="@(MediaData is not null ? () => NavigationManager.NavigateTo($"/media/{MediaData.Id}") : null)" style="cursor: @(MediaData is null ? "default" : "pointer")">
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<PictureComponent Picture="_poster" Height="60" Placeholder="/assets/media_poster.png" AlternativeText="poster"/>
|
||||
<div class="d-flex-inline flex-column">
|
||||
<h2 id="primaryText" class="m-0">
|
||||
@if (MediaData is null)
|
||||
{
|
||||
<span class="fw-bold">New @(MediaType)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span><span class="fw-bold">@(MediaData.Title)</span>@(MediaData.ReleaseDate.HasValue ? $" ({MediaData.ReleaseDate.Value.Year})" : string.Empty)</span>
|
||||
}
|
||||
</h2>
|
||||
<span id="secondaryText" class="text-secondary">Media settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using WatchIt.Common.Model.Media;
|
||||
using WatchIt.Website.Services.Client.Media;
|
||||
|
||||
namespace WatchIt.Website.Components.Pages.MediaEditPage.Panels;
|
||||
|
||||
public partial class MediaEditPageHeaderPanelComponent : ComponentBase
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
[Inject] public IMediaClientService MediaClientService { get; set; } = default!;
|
||||
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PARAMETERS
|
||||
|
||||
[Parameter] public required MediaResponse? MediaData { get; set; }
|
||||
[Parameter] public required string MediaType { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region FIELDS
|
||||
|
||||
private MediaPosterResponse? _poster;
|
||||
private List<KeyValuePair<string, object>> _attr = new List<KeyValuePair<string, object>>();
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
if (MediaData is not null)
|
||||
{
|
||||
await MediaClientService.GetMediaPoster(MediaData.Id, data => _poster = data);
|
||||
_attr.Add(new KeyValuePair<string, object>("@onclick", () => NavigationManager.NavigateTo($"/media/{MediaData.Id}")));
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* IDS */
|
||||
|
||||
#primaryText {
|
||||
margin-top: -8px !important;
|
||||
}
|
||||
|
||||
#secondaryText {
|
||||
color: lightgray !important;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@using Blazorise.Extensions
|
||||
@using WatchIt.Common.Model.Genres
|
||||
|
||||
|
||||
<div class="panel">
|
||||
<div class="vstack gap-3">
|
||||
<h4 class="fw-bold">Genres</h4>
|
||||
<div class="d-flex gap-3">
|
||||
<InputSelect class="w-100 form-control" TValue="short?" @bind-Value="@(_selectedGenre)">
|
||||
<option value="@(default(short?))" selected hidden="hidden">Choose genre...</option>
|
||||
@foreach (GenreResponse genre in _availableGenres)
|
||||
{
|
||||
<option value="@(genre.Id)">@(genre.Name)</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<button class="btn btn-secondary" @onclick="AddGenre" disabled="@(_selectedGenre is null || _addLoading || _chosenGenres.Values.Any(x => x))">
|
||||
<LoadingButtonContentComponent Content="Add" LoadingContent="Adding..." IsLoading="@(_addLoading)"/>
|
||||
</button>
|
||||
</div>
|
||||
@if (_chosenGenres.IsNullOrEmpty())
|
||||
{
|
||||
<span class="text-center">No items</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-transparent">
|
||||
<tbody>
|
||||
@foreach (KeyValuePair<GenreResponse, bool> genre in _chosenGenres)
|
||||
{
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
@(genre.Key.Name)
|
||||
</td>
|
||||
<td class="align-middle table-cell-fit">
|
||||
<button class="btn btn-outline-danger btn-sm w-100" type="button" disabled="@(_addLoading || genre.Value)" @onclick="@(() => RemoveGenre(genre.Key))">
|
||||
<LoadingButtonContentComponent IsLoading="@(genre.Value)">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</LoadingButtonContentComponent>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,83 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using WatchIt.Common.Model.Genres;
|
||||
using WatchIt.Common.Model.Media;
|
||||
using WatchIt.Website.Services.Client.Genres;
|
||||
using WatchIt.Website.Services.Client.Media;
|
||||
|
||||
namespace WatchIt.Website.Components.Pages.MediaEditPage.Panels;
|
||||
|
||||
public partial class MediaGenresEditPanelComponent : ComponentBase
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
[Inject] private IGenresClientService GenresClientService { get; set; } = default!;
|
||||
[Inject] private IMediaClientService MediaClientService { get; set; } = default!;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PARAMETERS
|
||||
|
||||
[Parameter] public required MediaResponse Data { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region FIELDS
|
||||
|
||||
private Dictionary<GenreResponse, bool> _chosenGenres = new Dictionary<GenreResponse, bool>();
|
||||
private List<GenreResponse> _availableGenres = new List<GenreResponse>();
|
||||
|
||||
private short? _selectedGenre;
|
||||
private bool _addLoading;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
foreach (GenreResponse genre in Data.Genres)
|
||||
{
|
||||
_chosenGenres[genre] = false;
|
||||
}
|
||||
await GenresClientService.GetGenres(successAction: data =>
|
||||
{
|
||||
IEnumerable<short> tempSelected = _chosenGenres.Keys.Select(x => x.Id);
|
||||
_availableGenres.AddRange(data.Where(x => !tempSelected.Contains(x.Id)));
|
||||
});
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddGenre()
|
||||
{
|
||||
_addLoading = true;
|
||||
await MediaClientService.PostMediaGenre(Data.Id, _selectedGenre!.Value, () =>
|
||||
{
|
||||
GenreResponse selectedGenre = _availableGenres.First(x => x.Id == _selectedGenre);
|
||||
_availableGenres.Remove(selectedGenre);
|
||||
_chosenGenres[selectedGenre] = false;
|
||||
_addLoading = false;
|
||||
_selectedGenre = null;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RemoveGenre(GenreResponse genre)
|
||||
{
|
||||
_chosenGenres[genre] = true;
|
||||
await MediaClientService.DeleteMediaGenre(Data.Id, genre.Id, () =>
|
||||
{
|
||||
_chosenGenres.Remove(genre);
|
||||
_availableGenres.Add(genre);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<AccountPictureComponent @ref="_accountPicture" Id="@(AccountData.Id)" Size="60"/>
|
||||
<div class="d-flex-inline flex-column">
|
||||
<h2 id="username" class="fw-bold m-0">@(AccountData.Username)</h2>
|
||||
<h2 id="primaryText" class="fw-bold m-0">@(AccountData.Username)</h2>
|
||||
<span id="secondaryText" class="text-secondary">User settings</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* IDS */
|
||||
|
||||
#username {
|
||||
#primaryText {
|
||||
margin-top: -8px !important;
|
||||
}
|
||||
|
||||
|
||||
54
WatchIt.Website/WatchIt.Website/Pages/GenrePage.razor
Normal file
54
WatchIt.Website/WatchIt.Website/Pages/GenrePage.razor
Normal file
@@ -0,0 +1,54 @@
|
||||
@using System.Text
|
||||
@using WatchIt.Website.Components.Common.ListComponent
|
||||
|
||||
@page "/genre/{id:int}"
|
||||
|
||||
@{
|
||||
StringBuilder sb = new StringBuilder(" - WatchIt");
|
||||
|
||||
if (!_loaded) sb.Insert(0, "Loading...");
|
||||
else if (_data is null) sb.Insert(0, "Error");
|
||||
else sb.Insert(0, $"\"{_data.Name}\" genre");
|
||||
|
||||
<PageTitle>@(sb.ToString())</PageTitle>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@if (!_loaded)
|
||||
{
|
||||
<div class="m-5">
|
||||
<LoadingComponent/>
|
||||
</div>
|
||||
}
|
||||
else if (_data is null)
|
||||
{
|
||||
<ErrorPanelComponent ErrorMessage="@($"Genre with ID {Id} was not found")"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ListComponent TItem="MediaResponse"
|
||||
TQuery="MediaQueryParameters"
|
||||
Title="@(_data.Name)"
|
||||
IdSource="@(item => item.Id)"
|
||||
NameSource="@(item => item.Title)"
|
||||
AdditionalNameInfoSource="@(item => item.ReleaseDate.HasValue ? $" ({item.ReleaseDate.Value.Year})" : null)"
|
||||
RatingSource="@(item => item.Rating)"
|
||||
UrlIdTemplate="/media/{0}"
|
||||
PictureDownloadingTask="@((id, action) => MediaClientService.GetMediaPoster(id, action))"
|
||||
ItemDownloadingTask="@((query, action) => GenresClientService.GetGenreMedia(_data.Id, query, action))"
|
||||
SortingOptions="@(new Dictionary<string, string>
|
||||
{
|
||||
{ "rating.count", "Number of ratings" },
|
||||
{ "rating.average", "Average rating" },
|
||||
{ "title", "Title" },
|
||||
{ "release_date", "Release date" },
|
||||
})"
|
||||
PosterPlaceholder="/assets/media_poster.png"
|
||||
GetGlobalRatingMethod="@((id, action) => MediaClientService.GetMediaRating(id, action))"
|
||||
GetUserRatingMethod="@((id, userId, successAction, notfoundAction) => MediaClientService.GetMediaRatingByUser(id, userId, successAction, notfoundAction))"
|
||||
PutRatingMethod="@((id, request) => MediaClientService.PutMediaRating(id, request))"
|
||||
DeleteRatingMethod="@(id => MediaClientService.DeleteMediaRating(id))">
|
||||
<MediaFilterFormComponent/>
|
||||
</ListComponent>
|
||||
}
|
||||
55
WatchIt.Website/WatchIt.Website/Pages/GenrePage.razor.cs
Normal file
55
WatchIt.Website/WatchIt.Website/Pages/GenrePage.razor.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using WatchIt.Common.Model.Genres;
|
||||
using WatchIt.Website.Layout;
|
||||
using WatchIt.Website.Services.Client.Genres;
|
||||
using WatchIt.Website.Services.Client.Media;
|
||||
|
||||
namespace WatchIt.Website.Pages;
|
||||
|
||||
public partial class GenrePage : ComponentBase
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
[Inject] private IGenresClientService GenresClientService { get; set; } = default!;
|
||||
[Inject] private IMediaClientService MediaClientService { get; set; } = default!;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PARAMETERS
|
||||
|
||||
[Parameter] public int Id { get; set; }
|
||||
|
||||
[CascadingParameter] public MainLayout Layout { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region FIELDS
|
||||
|
||||
private bool _loaded;
|
||||
private GenreResponse? _data;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Layout.BackgroundPhoto = null;
|
||||
|
||||
await GenresClientService.GetGenre(Id, data => _data = data);
|
||||
|
||||
_loaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
@using System.Text
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
@using WatchIt.Common.Model.Movies
|
||||
@using WatchIt.Common.Model.Photos
|
||||
@using WatchIt.Common.Model.Series
|
||||
@@ -7,76 +8,53 @@
|
||||
@page "/media/{id:long}/edit"
|
||||
@page "/media/new/{type?}"
|
||||
|
||||
@{
|
||||
StringBuilder sb = new StringBuilder(" - WatchIt");
|
||||
|
||||
<PageTitle>
|
||||
WatchIt -
|
||||
@if (_loaded)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_error) && _user?.IsAdmin == true)
|
||||
{
|
||||
if (_media is not null)
|
||||
{
|
||||
@($"Edit \"")@(_media.Title)@("\"")
|
||||
}
|
||||
if (!_loaded) sb.Insert(0, "Loading...");
|
||||
else if (!string.IsNullOrWhiteSpace(_error) || _user?.IsAdmin != true) sb.Insert(0, "Error");
|
||||
else
|
||||
{
|
||||
if (_movieRequest is null)
|
||||
{
|
||||
@("New TV series")
|
||||
}
|
||||
if (_media is not null) sb.Insert(0, $"Edit \"{_media.Title}\"");
|
||||
else
|
||||
{
|
||||
@("New movie")
|
||||
if (_movieRequest is null) sb.Insert(0, "TV series");
|
||||
else sb.Insert(0, "movie");
|
||||
sb.Insert(0, "New ");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@("Error")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@("Loading")
|
||||
}
|
||||
</PageTitle>
|
||||
|
||||
<div class="container-grid">
|
||||
@if (_loaded)
|
||||
<PageTitle>@(sb.ToString())</PageTitle>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="vstack gap-default">
|
||||
@if (!_loaded)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_error))
|
||||
<div class="m-5">
|
||||
<LoadingComponent/>
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(_error))
|
||||
{
|
||||
if (_user?.IsAdmin == true)
|
||||
<ErrorPanelComponent ErrorMessage="@_error"/>
|
||||
}
|
||||
else if (_user?.IsAdmin != true)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="rounded-3 panel panel-regular p-2">
|
||||
<div class="m-0 mx-2 mb-1 p-0">
|
||||
@if (_media is not null)
|
||||
{
|
||||
<a class="text-decoration-none text-reset" href="/media/@(_media.Id)">
|
||||
<h3>Edit @(_movieRequest is not null ? "movie" : "series") "@(_media.Title)"</h3>
|
||||
</a>
|
||||
<ErrorPanelComponent ErrorMessage="You do not have permission to view this site"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h3>Create new @(_movieRequest is not null ? "movie" : "series")</h3>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 gx-3">
|
||||
<div class="col-auto">
|
||||
<MediaEditPageHeaderPanelComponent MediaData="@(_media)"
|
||||
MediaType="@(_movieRequest is null ? "TV series" : "movie")"/>
|
||||
<div class="d-flex align-items-stretch gap-3">
|
||||
<PictureEditorPanelComponent Id="@(Id)"
|
||||
PictureGetTask="@(async (id, action) => await MediaClientService.GetMediaPoster(id, action))"
|
||||
PicturePutTask="@(async (id, data, action) => await MediaClientService.PutMediaPoster(id, new MediaPosterRequest(data), action))"
|
||||
PictureDeleteTask="@(async (id, action) => await MediaClientService.DeleteMediaPoster(id, action))"
|
||||
PicturePlaceholder="/assets/media_poster.png"
|
||||
Class="h-100"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="rounded-3 panel panel-regular p-4 h-100">
|
||||
PicturePlaceholder="/assets/media_poster.png"/>
|
||||
<div class="rounded-3 panel panel-regular p-4 w-100">
|
||||
<EditForm Model="_mediaRequest">
|
||||
<AntiforgeryToken/>
|
||||
<div class="container-grid">
|
||||
@@ -162,21 +140,29 @@
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<Tabs Pills
|
||||
RenderMode="TabsRenderMode.LazyLoad"
|
||||
Class="panel panel-menu panel-background-menu justify-content-center"
|
||||
SelectedTab="genres">
|
||||
<Items>
|
||||
<Tab Name="genres">Genres</Tab>
|
||||
<Tab Name="actors">Actors</Tab>
|
||||
<Tab Name="creators">Creators</Tab>
|
||||
<Tab Name="photos">Photos</Tab>
|
||||
</Items>
|
||||
<Content>
|
||||
<TabPanel Name="genres">
|
||||
<MediaGenresEditPanelComponent Data="@(_media)"/>
|
||||
</TabPanel>
|
||||
<TabPanel Name="actors">
|
||||
<MediaActorRolesEditPanelComponent Id="@(Id)"
|
||||
Persons="@(_persons)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
</TabPanel>
|
||||
<TabPanel Name="creators">
|
||||
<MediaCreatorRolesEditPanelComponent Id="@(Id)"
|
||||
Persons="@(_persons)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
</TabPanel>
|
||||
<TabPanel Name="photos">
|
||||
<div class="rounded-3 panel panel-regular p-4">
|
||||
<div class="container-grid">
|
||||
<div class="row mb-3">
|
||||
@@ -333,35 +319,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ErrorPanelComponent ErrorMessage="You do not have permission to view this site"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ErrorPanelComponent ErrorMessage="@_error"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="m-5">
|
||||
<LoadingComponent/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Content>
|
||||
</Tabs>
|
||||
}
|
||||
</div>
|
||||
@@ -10,6 +10,7 @@ using WatchIt.Website.Services.Configuration;
|
||||
using WatchIt.Website.Services.Tokens;
|
||||
using WatchIt.Website.Services.Client.Accounts;
|
||||
using WatchIt.Website.Services.Client.Genders;
|
||||
using WatchIt.Website.Services.Client.Genres;
|
||||
using WatchIt.Website.Services.Client.Media;
|
||||
using WatchIt.Website.Services.Client.Movies;
|
||||
using WatchIt.Website.Services.Client.Persons;
|
||||
@@ -75,6 +76,7 @@ public static class Program
|
||||
// WebAPI
|
||||
builder.Services.AddScoped<IAccountsClientService, AccountsClientService>();
|
||||
builder.Services.AddSingleton<IGendersClientService, GendersClientService>();
|
||||
builder.Services.AddSingleton<IGenresClientService, GenresClientService>();
|
||||
builder.Services.AddSingleton<IMediaClientService, MediaClientService>();
|
||||
builder.Services.AddSingleton<IMoviesClientService, MoviesClientService>();
|
||||
builder.Services.AddSingleton<ISeriesClientService, SeriesClientService>();
|
||||
|
||||
@@ -46,11 +46,12 @@
|
||||
},
|
||||
"Genres": {
|
||||
"Base": "/genres",
|
||||
"GetAll": "",
|
||||
"Get": "/{0}",
|
||||
"Post": "",
|
||||
"Put": "/{0}",
|
||||
"Delete": "/{0}"
|
||||
"GetGenres": "",
|
||||
"GetGenre": "/{0}",
|
||||
"PostGenre": "",
|
||||
"PutGenre": "/{0}",
|
||||
"DeleteGenre": "/{0}",
|
||||
"GetGenreMedia": "/{0}/media"
|
||||
},
|
||||
"Media": {
|
||||
"Base": "/media",
|
||||
|
||||
Reference in New Issue
Block a user