MediaEditPage - photos management panel added

This commit is contained in:
2024-09-27 01:05:06 +02:00
Unverified
parent d02207a335
commit b56261b672
18 changed files with 403 additions and 19 deletions

View File

@@ -1,10 +1,28 @@
using WatchIt.Common.Model.Photos; using System.Diagnostics.CodeAnalysis;
using WatchIt.Common.Model.Photos;
using WatchIt.Database.Model.Media; using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Media;
public class MediaPhotoRequest : Photo public class MediaPhotoRequest : Photo
{ {
#region CONSTRUCTORS
public MediaPhotoRequest() {}
[SetsRequiredMembers]
public MediaPhotoRequest(PhotoResponse response)
{
Image = response.Image;
MimeType = response.MimeType;
}
#endregion
#region PUBLIC METHODS
public MediaPhotoImage CreateMediaPhotoImage(long mediaId) => new MediaPhotoImage public MediaPhotoImage CreateMediaPhotoImage(long mediaId) => new MediaPhotoImage
{ {
MediaId = mediaId, MediaId = mediaId,
@@ -19,4 +37,6 @@ public class MediaPhotoRequest : Photo
FirstGradientColor = Background.FirstGradientColor, FirstGradientColor = Background.FirstGradientColor,
SecondGradientColor = Background.SecondGradientColor SecondGradientColor = Background.SecondGradientColor
}; };
#endregion
} }

View File

@@ -4,9 +4,4 @@ namespace WatchIt.Common.Model.Media;
public abstract class MediaPoster : Picture public abstract class MediaPoster : Picture
{ {
#region PUBLIC METHODS
public override string ToString() => $"data:{MimeType};base64,{Convert.ToBase64String(Image)}";
#endregion
} }

View File

@@ -1,9 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using WatchIt.Database.Model.Media; using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Photos; namespace WatchIt.Common.Model.Photos;
public class PhotoBackgroundDataRequest : PhotoBackgroundData public class PhotoBackgroundDataRequest : PhotoBackgroundData
{ {
#region CONSTRUCTORS
public PhotoBackgroundDataRequest() {}
[SetsRequiredMembers]
public PhotoBackgroundDataRequest(PhotoBackgroundData photoBackgroundData)
{
IsUniversalBackground = photoBackgroundData.IsUniversalBackground;
FirstGradientColor = photoBackgroundData.FirstGradientColor;
SecondGradientColor = photoBackgroundData.SecondGradientColor;
}
#endregion
#region PUBLIC METHODS #region PUBLIC METHODS
public MediaPhotoImageBackground CreateMediaPhotoImageBackground(Guid photoId) => new MediaPhotoImageBackground public MediaPhotoImageBackground CreateMediaPhotoImageBackground(Guid photoId) => new MediaPhotoImageBackground

View File

@@ -38,7 +38,7 @@ public class PhotoQueryParameters : QueryParameters<PhotoResponse>
&& &&
TestBoolean(item.Background is not null, IsBackground) TestBoolean(item.Background is not null, IsBackground)
&& &&
TestBoolean(item.Background!.IsUniversalBackground, IsUniversalBackground) TestBoolean(item.Background is not null && item.Background.IsUniversalBackground, IsUniversalBackground)
&& &&
TestComparable(item.UploadDate, UploadDate, UploadDateFrom, UploadDateTo) TestComparable(item.UploadDate, UploadDate, UploadDateFrom, UploadDateTo)
); );

View File

@@ -13,4 +13,12 @@ public abstract class Picture
public required string MimeType { get; set; } public required string MimeType { get; set; }
#endregion #endregion
#region PUBLIC METHODS
public override string ToString() => $"data:{MimeType};base64,{Convert.ToBase64String(Image)}";
#endregion
} }

View File

@@ -55,6 +55,7 @@ public class PhotosController : ControllerBase
[HttpPut("{id}/background_data")] [HttpPut("{id}/background_data")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]

View File

@@ -241,7 +241,10 @@ public class MediaWebAPIService : BaseWebAPIService, IMediaWebAPIService
{ {
string url = GetUrl(EndpointsConfiguration.Media.PostMediaPhoto, mediaId); string url = GetUrl(EndpointsConfiguration.Media.PostMediaPhoto, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Post, url); HttpRequest request = new HttpRequest(HttpMethodType.Post, url)
{
Body = data
};
HttpResponse response = await _httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)

View File

@@ -5,7 +5,7 @@ namespace WatchIt.Website.Services.WebAPI.Photos;
public interface IPhotosWebAPIService public interface IPhotosWebAPIService
{ {
Task GetPhotoRandomBackground(Action<PhotoResponse>? successAction = null, Action? notFoundAction = null); Task GetPhotoRandomBackground(Action<PhotoResponse>? successAction = null, Action? notFoundAction = null);
Task DeletePhoto(Guid id, Action<PhotoResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null); Task DeletePhoto(Guid id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null);
Task PutPhotoBackgroundData(Guid id, PhotoBackgroundDataRequest data, Action<PhotoResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null); Task PutPhotoBackgroundData(Guid id, PhotoBackgroundDataRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null);
Task DeletePhotoBackgroundData(Guid id, Action<PhotoResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null); Task DeletePhotoBackgroundData(Guid id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null);
} }

View File

@@ -42,7 +42,7 @@ public class PhotosWebAPIService : BaseWebAPIService, IPhotosWebAPIService
.ExecuteAction(); .ExecuteAction();
} }
public async Task DeletePhoto(Guid id, Action<PhotoResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null) public async Task DeletePhoto(Guid id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Photos.DeletePhoto, id); string url = GetUrl(EndpointsConfiguration.Photos.DeletePhoto, id);
@@ -60,7 +60,7 @@ public class PhotosWebAPIService : BaseWebAPIService, IPhotosWebAPIService
#region Background data #region Background data
public async Task PutPhotoBackgroundData(Guid id, PhotoBackgroundDataRequest data, Action<PhotoResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null) public async Task PutPhotoBackgroundData(Guid id, PhotoBackgroundDataRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Photos.PutPhotoBackgroundData, id); string url = GetUrl(EndpointsConfiguration.Photos.PutPhotoBackgroundData, id);
@@ -71,13 +71,14 @@ public class PhotosWebAPIService : BaseWebAPIService, IPhotosWebAPIService
HttpResponse response = await _httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction) .RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction) .RegisterActionFor403Forbidden(forbiddenAction)
.RegisterActionFor404NotFound(notFoundAction) .RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction(); .ExecuteAction();
} }
public async Task DeletePhotoBackgroundData(Guid id, Action<PhotoResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null) public async Task DeletePhotoBackgroundData(Guid id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Photos.DeletePhotoBackgroundData, id); string url = GetUrl(EndpointsConfiguration.Photos.DeletePhotoBackgroundData, id);

View File

@@ -9,8 +9,8 @@
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png"/>
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="app.css?version=0.1.0.16"/> <link rel="stylesheet" href="app.css?version=0.2.0.2"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.1.0.26"/> <link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.2.0.9"/>
<!-- BOOTSTRAP --> <!-- BOOTSTRAP -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>

View File

@@ -1,4 +1,6 @@
@using WatchIt.Common.Model.Movies @using Microsoft.IdentityModel.Tokens
@using WatchIt.Common.Model.Movies
@using WatchIt.Common.Model.Photos
@using WatchIt.Common.Model.Series @using WatchIt.Common.Model.Series
@page "/media/{id:long}/edit" @page "/media/{id:long}/edit"
@@ -197,6 +199,166 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row mt-3">
<div class="col">
<div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid p-0">
<div class="row mb-3">
<div class="col">
<div class="d-flex align-items-center h-100">
<h4 class="m-0"><strong>Photos</strong></h4>
</div>
</div>
<div class="col-auto">
@if (!_photoEditMode)
{
<button type="button" class="btn btn-secondary" disabled="@(!Id.HasValue)" @onclick="() => InitEditPhoto(null)">Add new photo</button>
}
else
{
<div class="d-flex gap-3 align-items-center">
@if (!string.IsNullOrWhiteSpace(_photoEditError))
{
<div class="text-danger">
@_photoEditError
</div>
}
<button type="button" class="btn btn-secondary" disabled="@(_photoEditSaving)" @onclick="SaveEditPhoto">
@if (!_photoEditSaving)
{
<span class="sr-only">Save</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Saving...</span>
}
</button>
<button type="button" class="btn btn-secondary" disabled="@(_photoEditSaving)" @onclick="CancelEditPhoto">Cancel</button>
</div>
}
</div>
</div>
<div class="row">
<div class="col">
@if (!_photoEditMode)
{
if (!_photos.IsNullOrEmpty())
{
<div id="scrollPhotos" class="d-flex p-3 gap-3" data-bs-spy="scroll" tabindex="0">
@foreach (PhotoResponse photo in _photos)
{
<div class="container-fluid p-0 m-0 photo-container">
<div class="row">
<div class="col">
<img class="rounded-1 shadow object-fit-cover photo-default-aspect-ratio" src="@(photo.ToString())" alt="photo" width="350"/>
</div>
</div>
<div class="row mt-2 gx-2">
@if (photo.Background is not null)
{
<div class="col-auto">
<div class="d-flex align-items-center">
<div id="backgroundIndicator" class="border rounded-circle circle-@(photo.Background.IsUniversalBackground ? "blue" : "grey") p-1" data-toggle="tooltip" data-placement="top" title="@(photo.Background.IsUniversalBackground ? "Universal" : "Media-only") background">
<img class="no-vertical-align" src="assets/icons/background.png" alt="background_icon" height="20px" width="20px"/>
</div>
</div>
</div>
}
<div class="col">
<div class="d-flex align-items-center h-100 text-size-upload-date">
Upload: @(photo.UploadDate.ToString())
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-secondary btn-sm" @onclick="() => InitEditPhoto(photo.Id)" disabled="@(_photoDeleting.Contains(photo.Id))">
<img src="assets/icons/edit.png" alt="edit_icon" height="20px" width="20px"/>
</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-danger btn-sm" disabled="@(_photoDeleting.Contains(photo.Id))" @onclick="() => DeletePhoto(photo.Id)">
@if (!_photoDeleting.Contains(photo.Id))
{
<img src="assets/icons/delete.png" alt="delete_icon" height="20px" width="20px"/>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
}
</button>
</div>
</div>
</div>
}
</div>
}
else
{
<div class="d-flex justify-content-center">
Photo list is empty
</div>
}
}
else
{
<div class="container-fluid p-0 m-0">
<div class="row">
<div class="col-auto">
<div class="container-fluid p-0 m-0">
<div class="row">
<div class="col">
<img class="rounded-1 shadow object-fit-cover photo-default-aspect-ratio" src="@(_photoEditRequest is null ? "assets/photo.png" : _photoEditRequest.ToString())" alt="edit_photo" width="300px"/>
</div>
</div>
@if (_photoEditId is null)
{
<div class="row mt-2">
<div class="col">
<InputFile class="form-control" OnChange="LoadPhoto" autocomplete="off" style="width: 300px;"/>
</div>
</div>
}
</div>
</div>
<div class="col">
<div class="container-fluid p-0 m-0">
<div class="row form-group">
<div class="col">
<div class="form-check">
<InputCheckbox class="form-check-input" @bind-Value="_photoEditIsBackground"/>
<label class="form-check-label">Use as background</label>
</div>
</div>
<div class="col">
<div class="form-check">
<InputCheckbox class="form-check-input" @bind-Value="_photoEditBackgroundData.IsUniversalBackground" disabled="@(!_photoEditIsBackground)"/>
<label class="form-check-label">Use as universal background</label>
</div>
</div>
</div>
<div class="row form-group my-1">
<label for="first-gradient-color" class="col-4 col-form-label">First gradient color</label>
<div class="col-8">
<input type="color" class="form-control form-control-color w-100" id="first-gradient-color" value="#@(Convert.ToHexString(_photoEditBackgroundData.FirstGradientColor))" disabled="@(!_photoEditIsBackground)" @onchange="EditPhotoFirstGradientColorChanged">
</div>
</div>
<div class="row form-group">
<label for="second-gradient-color" class="col-4 col-form-label">Second gradient color</label>
<div class="col-8">
<input type="color" class="form-control form-control-color w-100" id="second-gradient-color" value="#@(Convert.ToHexString(_photoEditBackgroundData.SecondGradientColor))" disabled="@(!_photoEditIsBackground)" @onchange="EditPhotoSecondGradientColorChanged">
</div>
</div>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
</div>
</div>
} }
else else
{ {
@@ -220,7 +382,7 @@
{ {
<style> <style>
body { body {
background-image: url('@($"data:{_background.MimeType};base64,{Convert.ToBase64String(_background.Image)}")') !important; background-image: url('@(_background.ToString())') !important;
} }
.logo, .main-button { .logo, .main-button {

View File

@@ -22,6 +22,7 @@ public partial class MediaEditPage : ComponentBase
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!; [Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IMoviesWebAPIService MoviesWebAPIService { get; set; } = default!; [Inject] public IMoviesWebAPIService MoviesWebAPIService { get; set; } = default!;
[Inject] public ISeriesWebAPIService SeriesWebAPIService { get; set; } = default!; [Inject] public ISeriesWebAPIService SeriesWebAPIService { get; set; } = default!;
[Inject] public IPhotosWebAPIService PhotosWebAPIService { get; set; } = default!;
#endregion #endregion
@@ -58,6 +59,21 @@ public partial class MediaEditPage : ComponentBase
private bool _mediaPosterSaving; private bool _mediaPosterSaving;
private bool _mediaPosterDeleting; private bool _mediaPosterDeleting;
private IEnumerable<PhotoResponse> _photos = new List<PhotoResponse>();
private List<Guid> _photoDeleting = new List<Guid>();
private bool _photoEditMode;
private string? _photoEditError;
private Guid? _photoEditId;
private bool _photoEditSaving;
private bool _photoEditIsBackground;
private MediaPhotoRequest? _photoEditRequest;
private PhotoBackgroundDataRequest? _photoEditBackgroundData = new PhotoBackgroundDataRequest()
{
FirstGradientColor = [0xFF, 0xFF, 0xFF],
SecondGradientColor = [0x00, 0x00, 0x00],
IsUniversalBackground = false
};
#endregion #endregion
@@ -101,7 +117,8 @@ public partial class MediaEditPage : ComponentBase
{ {
_mediaPosterSaved = data; _mediaPosterSaved = data;
_mediaPosterRequest = new MediaPosterRequest(data); _mediaPosterRequest = new MediaPosterRequest(data);
}) }),
MediaWebAPIService.GetMediaPhotos(Id.Value, successAction: data => _photos = data)
]); ]);
} }
@@ -252,5 +269,116 @@ public partial class MediaEditPage : ComponentBase
#endregion #endregion
#region Photos
private async Task DeletePhoto(Guid id)
{
async Task Success()
{
NavigationManager.Refresh(true);
}
_photoDeleting.Add(id);
await PhotosWebAPIService.DeletePhoto(id, async () => await Success());
}
private void InitEditPhoto(Guid? id)
{
_photoEditMode = true;
_photoEditId = id;
_photoEditRequest = null;
_photoEditIsBackground = false;
_photoEditBackgroundData = new PhotoBackgroundDataRequest
{
FirstGradientColor = [0xFF, 0xFF, 0xFF],
SecondGradientColor = [0x00, 0x00, 0x00],
IsUniversalBackground = false
};
if (id is not null)
{
PhotoResponse response = _photos.First(x => x.Id == id);
_photoEditRequest = new MediaPhotoRequest(response);
if (response.Background is not null)
{
_photoEditIsBackground = true;
_photoEditBackgroundData = new PhotoBackgroundDataRequest(response.Background);
}
}
}
private void CancelEditPhoto()
{
_photoEditMode = false;
_photoEditId = null;
_photoEditError = null;
}
private async Task SaveEditPhoto()
{
void Success()
{
NavigationManager.Refresh(true);
}
void BadRequest(IDictionary<string, string[]> errors)
{
_photoEditError = errors.SelectMany(x => x.Value).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(_basicDataError))
{
_photoEditSaving = false;
}
}
_photoEditSaving = true;
if (_photoEditId is null)
{
_photoEditRequest.Background = _photoEditIsBackground ? _photoEditBackgroundData : null;
await MediaWebAPIService.PostMediaPhoto(Id.Value, _photoEditRequest, Success, BadRequest);
}
else
{
if (_photoEditIsBackground)
{
await PhotosWebAPIService.PutPhotoBackgroundData(_photoEditId.Value, _photoEditBackgroundData, Success, BadRequest);
}
else
{
await PhotosWebAPIService.DeletePhotoBackgroundData(_photoEditId.Value, Success);
}
}
}
private async Task LoadPhoto(InputFileChangeEventArgs args)
{
if (args.File.ContentType.StartsWith("image"))
{
Stream stream = args.File.OpenReadStream(5242880);
byte[] array;
using (MemoryStream ms = new MemoryStream())
{
await stream.CopyToAsync(ms);
array = ms.ToArray();
}
_photoEditRequest = new MediaPhotoRequest
{
Image = array,
MimeType = args.File.ContentType
};
}
}
private void EditPhotoFirstGradientColorChanged(ChangeEventArgs e)
{
_photoEditBackgroundData.FirstGradientColor = Convert.FromHexString(e.Value.ToString().Replace("#", string.Empty));
}
private void EditPhotoSecondGradientColorChanged(ChangeEventArgs e)
{
_photoEditBackgroundData.SecondGradientColor = Convert.FromHexString(e.Value.ToString().Replace("#", string.Empty));
}
#endregion
#endregion #endregion
} }

View File

@@ -0,0 +1,45 @@
/* IDS */
#scrollPhotos {
overflow-x: scroll;
border-color: grey;
border-width: 1px;
border-style: solid;
border-radius: 10px;
}
#backgroundIndicator {
height: 30px;
width: 30px;
}
/* CLASSES */
.circle-blue {
background-color: dodgerblue;
border-color: dodgerblue;
}
.circle-grey {
background-color: grey;
border-color: grey;
}
.no-vertical-align {
vertical-align: inherit;
}
.photo-container {
width: 350px;
}
.text-size-upload-date {
font-size: 12px;
}
.photo-default-aspect-ratio {
aspect-ratio: 16/9;
}

View File

@@ -93,3 +93,7 @@ body, html {
.btn-stretch-x { .btn-stretch-x {
width: 100%; width: 100%;
} }
.w-100 {
width: 100%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB