Merge pull request #85 from mateuszskoczek/features/searching

Features/searching
This commit is contained in:
2024-09-29 15:51:56 +02:00
committed by GitHub
Unverified
41 changed files with 961 additions and 377 deletions

View File

@@ -2,20 +2,8 @@
namespace WatchIt.Common.Model.Accounts; namespace WatchIt.Common.Model.Accounts;
public abstract class AccountProfilePicture public abstract class AccountProfilePicture : Picture
{ {
#region PROPERTIES
[JsonPropertyName("image")]
public required byte[] Image { get; set; }
[JsonPropertyName("mime_type")]
public required string MimeType { get; set; }
#endregion
#region CONSTRUCTORS #region CONSTRUCTORS
[JsonConstructor] [JsonConstructor]

View File

@@ -1,9 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Rating;
public class MediaRatingRequest public class RatingRequest
{ {
#region PROPERTIES #region PROPERTIES
@@ -17,7 +17,7 @@ public class MediaRatingRequest
#region CONSTRUCTORS #region CONSTRUCTORS
[SetsRequiredMembers] [SetsRequiredMembers]
public MediaRatingRequest(short rating) public RatingRequest(short rating)
{ {
Rating = rating; Rating = rating;
} }

View File

@@ -1,9 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Rating;
public class MediaRatingResponse public class RatingResponse
{ {
#region PROPERTIES #region PROPERTIES
@@ -20,10 +20,10 @@ public class MediaRatingResponse
#region CONSTRUCTORS #region CONSTRUCTORS
[JsonConstructor] [JsonConstructor]
public MediaRatingResponse() {} public RatingResponse() {}
[SetsRequiredMembers] [SetsRequiredMembers]
public MediaRatingResponse(double ratingAverage, long ratingCount) public RatingResponse(double ratingAverage, long ratingCount)
{ {
RatingAverage = ratingAverage; RatingAverage = ratingAverage;
RatingCount = ratingCount; RatingCount = ratingCount;

View File

@@ -66,7 +66,7 @@ public abstract class QueryParameters
( (
!string.IsNullOrEmpty(property) !string.IsNullOrEmpty(property)
&& &&
new Regex(regexQuery).IsMatch(property) Regex.IsMatch(property, regexQuery, RegexOptions.IgnoreCase)
) )
); );

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Model.Genres; using WatchIt.Common.Model.Genres;
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Photos; using WatchIt.Common.Model.Photos;
using WatchIt.Common.Model.Rating;
using WatchIt.WebAPI.Services.Controllers.Media; using WatchIt.WebAPI.Services.Controllers.Media;
namespace WatchIt.WebAPI.Controllers; namespace WatchIt.WebAPI.Controllers;
@@ -74,7 +75,7 @@ public class MediaController : ControllerBase
[HttpGet("{id}/rating")] [HttpGet("{id}/rating")]
[AllowAnonymous] [AllowAnonymous]
[ProducesResponseType(typeof(MediaRatingResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(RatingResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaRating([FromRoute] long id) => await _mediaControllerService.GetMediaRating(id); public async Task<ActionResult> GetMediaRating([FromRoute] long id) => await _mediaControllerService.GetMediaRating(id);
@@ -90,7 +91,7 @@ public class MediaController : ControllerBase
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PutMediaRating([FromRoute] long id, [FromBody] MediaRatingRequest data) => await _mediaControllerService.PutMediaRating(id, data); public async Task<ActionResult> PutMediaRating([FromRoute] long id, [FromBody] RatingRequest data) => await _mediaControllerService.PutMediaRating(id, data);
[HttpDelete("{id}/rating")] [HttpDelete("{id}/rating")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

View File

@@ -1,5 +1,6 @@
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Photos; using WatchIt.Common.Model.Photos;
using WatchIt.Common.Model.Rating;
using WatchIt.WebAPI.Services.Controllers.Common; using WatchIt.WebAPI.Services.Controllers.Common;
namespace WatchIt.WebAPI.Services.Controllers.Media; namespace WatchIt.WebAPI.Services.Controllers.Media;
@@ -14,7 +15,7 @@ public interface IMediaControllerService
Task<RequestResult> GetMediaRating(long mediaId); Task<RequestResult> GetMediaRating(long mediaId);
Task<RequestResult> GetMediaRatingByUser(long mediaId, long userId); Task<RequestResult> GetMediaRatingByUser(long mediaId, long userId);
Task<RequestResult> PutMediaRating(long mediaId, MediaRatingRequest data); Task<RequestResult> PutMediaRating(long mediaId, RatingRequest data);
Task<RequestResult> DeleteMediaRating(long mediaId); Task<RequestResult> DeleteMediaRating(long mediaId);
Task<RequestResult> PostMediaView(long mediaId); Task<RequestResult> PostMediaView(long mediaId);

View File

@@ -3,6 +3,7 @@ using SimpleToolkit.Extensions;
using WatchIt.Common.Model.Genres; using WatchIt.Common.Model.Genres;
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Photos; using WatchIt.Common.Model.Photos;
using WatchIt.Common.Model.Rating;
using WatchIt.Database; using WatchIt.Database;
using WatchIt.Database.Model.Media; using WatchIt.Database.Model.Media;
using WatchIt.Database.Model.Rating; using WatchIt.Database.Model.Rating;
@@ -109,7 +110,7 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
double ratingAverage = item.RatingMedia.Any() ? item.RatingMedia.Average(x => x.Rating) : 0; double ratingAverage = item.RatingMedia.Any() ? item.RatingMedia.Average(x => x.Rating) : 0;
long ratingCount = item.RatingMedia.Count(); long ratingCount = item.RatingMedia.Count();
MediaRatingResponse ratingResponse = new MediaRatingResponse(ratingAverage, ratingCount); RatingResponse ratingResponse = new RatingResponse(ratingAverage, ratingCount);
return RequestResult.Ok(ratingResponse); return RequestResult.Ok(ratingResponse);
} }
@@ -131,7 +132,7 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
return RequestResult.Ok(rating.Value); return RequestResult.Ok(rating.Value);
} }
public async Task<RequestResult> PutMediaRating(long mediaId, MediaRatingRequest data) public async Task<RequestResult> PutMediaRating(long mediaId, RatingRequest data)
{ {
short ratingValue = data.Rating; short ratingValue = data.Rating;
if (ratingValue < 1 || ratingValue > 10) if (ratingValue < 1 || ratingValue > 10)

View File

@@ -38,7 +38,8 @@ public class MoviesControllerService : IMoviesControllerService
public async Task<RequestResult> GetAllMovies(MovieQueryParameters query) public async Task<RequestResult> GetAllMovies(MovieQueryParameters query)
{ {
IEnumerable<MovieResponse> data = await _database.MediaMovies.Select(x => new MovieResponse(x)).ToListAsync(); IEnumerable<MediaMovie> rawData = await _database.MediaMovies.ToListAsync();
IEnumerable<MovieResponse> data = rawData.Select(x => new MovieResponse(x));
data = query.PrepareData(data); data = query.PrepareData(data);
return RequestResult.Ok(data); return RequestResult.Ok(data);
} }

View File

@@ -38,7 +38,8 @@ public class SeriesControllerService : ISeriesControllerService
public async Task<RequestResult> GetAllSeries(SeriesQueryParameters query) public async Task<RequestResult> GetAllSeries(SeriesQueryParameters query)
{ {
IEnumerable<SeriesResponse> data = await _database.MediaSeries.Select(x => new SeriesResponse(x)).ToListAsync(); IEnumerable<MediaSeries> rawData = await _database.MediaSeries.ToListAsync();
IEnumerable<SeriesResponse> data = rawData.Select(x => new SeriesResponse(x));
data = query.PrepareData(data); data = query.PrepareData(data);
return RequestResult.Ok(data); return RequestResult.Ok(data);
} }

View File

@@ -1,6 +1,7 @@
using WatchIt.Common.Model.Genres; using WatchIt.Common.Model.Genres;
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Photos; using WatchIt.Common.Model.Photos;
using WatchIt.Common.Model.Rating;
namespace WatchIt.Website.Services.WebAPI.Media; namespace WatchIt.Website.Services.WebAPI.Media;
@@ -12,9 +13,9 @@ public interface IMediaWebAPIService
Task PostMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null); Task PostMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null);
Task DeleteMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null); Task DeleteMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null);
Task GetMediaRating(long mediaId, Action<MediaRatingResponse>? successAction = null, Action? notFoundAction = null); Task GetMediaRating(long mediaId, Action<RatingResponse>? successAction = null, Action? notFoundAction = null);
Task GetMediaRatingByUser(long mediaId, long userId, Action<short>? successAction = null, Action? notFoundAction = null); Task GetMediaRatingByUser(long mediaId, long userId, Action<short>? successAction = null, Action? notFoundAction = null);
Task PutMediaRating(long mediaId, MediaRatingRequest body, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null); Task PutMediaRating(long mediaId, RatingRequest body, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null);
Task DeleteMediaRating(long mediaId, Action? successAction = null, Action? unauthorizedAction = null); Task DeleteMediaRating(long mediaId, Action? successAction = null, Action? unauthorizedAction = null);
Task PostMediaView(long mediaId, Action? successAction = null, Action? notFoundAction = null); Task PostMediaView(long mediaId, Action? successAction = null, Action? notFoundAction = null);

View File

@@ -1,6 +1,7 @@
using WatchIt.Common.Model.Genres; using WatchIt.Common.Model.Genres;
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Photos; using WatchIt.Common.Model.Photos;
using WatchIt.Common.Model.Rating;
using WatchIt.Common.Services.HttpClient; using WatchIt.Common.Services.HttpClient;
using WatchIt.Website.Services.Utility.Configuration; using WatchIt.Website.Services.Utility.Configuration;
using WatchIt.Website.Services.Utility.Configuration.Model; using WatchIt.Website.Services.Utility.Configuration.Model;
@@ -93,7 +94,7 @@ public class MediaWebAPIService : BaseWebAPIService, IMediaWebAPIService
#region Rating #region Rating
public async Task GetMediaRating(long mediaId, Action<MediaRatingResponse>? successAction = null, Action? notFoundAction = null) public async Task GetMediaRating(long mediaId, Action<RatingResponse>? successAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.GetMediaRating, mediaId); string url = GetUrl(EndpointsConfiguration.Media.GetMediaRating, mediaId);
@@ -117,7 +118,7 @@ public class MediaWebAPIService : BaseWebAPIService, IMediaWebAPIService
.ExecuteAction(); .ExecuteAction();
} }
public async Task PutMediaRating(long mediaId, MediaRatingRequest body, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null) public async Task PutMediaRating(long mediaId, RatingRequest body, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.PutMediaRating, mediaId); string url = GetUrl(EndpointsConfiguration.Media.PutMediaRating, mediaId);

View File

@@ -9,13 +9,19 @@
<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.2.0.2"/> <link rel="stylesheet" href="css/general.css?version=0.2.0.3"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.2.0.9"/> <link rel="stylesheet" href="css/main_button.css?version=0.2.0.0"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.2.0.11"/>
<!-- 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>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- BLAZORISE -->
<link href="_content/Blazorise.Icons.FontAwesome/v6/css/all.min.css" rel="stylesheet">
<link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
<link href="_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css" rel="stylesheet" />
<!-- FONTS --> <!-- FONTS -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@@ -1,7 +1,5 @@
<div class="row"> <div class="rounded-3 panel panel-regular p-4">
<div class="col"> <div class="container-grid">
<div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
@@ -27,6 +25,4 @@
</div> </div>
} }
</div> </div>
</div>
</div>
</div> </div>

View File

@@ -0,0 +1,26 @@
<div class="container-grid">
<div class="row">
<div class="col-auto">
<img id="picture" class="rounded-2 shadow object-fit-cover picture-aspect-ratio" src="@(_picture is not null ? _picture.ToString() : "assets/poster.png")" alt="picture" height="@(PictureHeight)"/>
</div>
<div class="col">
<div class="d-flex align-items-start flex-column h-100">
<div class="mb-auto">
<span id="nameText">
<strong>@(Name)</strong>@(string.IsNullOrWhiteSpace(AdditionalNameInfo) ? string.Empty : AdditionalNameInfo)
</span>
</div>
<div class="d-inline-flex gap-2">
<span id="ratingStar">★</span>
<div class="d-inline-flex flex-column justify-content-center">
<span id="ratingValue">@(_rating is not null && _rating.RatingCount > 0 ? _rating.RatingAverage : "--")/10</span>
@if (_rating is not null && _rating.RatingCount > 0)
{
<span id="ratingCount">@(_rating.RatingCount)</span>
}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model;
using WatchIt.Common.Model.Rating;
namespace WatchIt.Website.Components;
public partial class ListItemComponent : ComponentBase
{
#region PARAMETERS
[Parameter] public required long Id { get; set; }
[Parameter] public required string Name { get; set; }
[Parameter] public string? AdditionalNameInfo { get; set; }
[Parameter] public required Func<long, Action<Picture>, Task> PictureDownloadingTask { get; set; }
[Parameter] public required Func<long, Action<RatingResponse>, Task> RatingDownloadingTask { get; set; }
[Parameter] public int PictureHeight { get; set; } = 150;
#endregion
#region FIELDS
private bool _loaded;
private Picture? _picture;
private RatingResponse? _rating;
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> endTasks = new List<Task>();
// STEP 0
endTasks.AddRange(
[
PictureDownloadingTask(Id, picture => _picture = picture),
RatingDownloadingTask(Id, rating => _rating = rating)
]);
await Task.WhenAll(endTasks);
_loaded = true;
StateHasChanged();
}
}
#endregion
}

View File

@@ -0,0 +1,17 @@
/* IDS */
#nameText {
font-size: 25px;
}
#ratingStar {
font-size: 30px;
}
#ratingValue {
font-size: 20px;
}
#ratingCount {
font-size: 10px;
}

View File

@@ -1,12 +1,8 @@
<div class="row"> <div class="d-flex flex-column">
<div class="col">
<div class="d-flex flex-column m-5">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div id="spinner" class="spinner-border text-dark"></div> <div id="spinner" class="spinner-border text-@(Color)"></div>
</div> </div>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<p id="text" class="text-dark">Loading...</p> <p id="text" class="text-@(Color)" m-0>Loading...</p>
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components;
namespace WatchIt.Website.Components;
public partial class LoadingComponent : ComponentBase
{
#region PARAMETERS
[Parameter] public string Color { get; set; } = "dark";
#endregion
}

View File

@@ -0,0 +1,82 @@
@using Microsoft.IdentityModel.Tokens
@typeparam TItem
@typeparam TQuery where TQuery : WatchIt.Common.Query.QueryParameters
<div class="rounded-3 panel panel-regular p-4">
<div class="container-grid">
<div class="row mb-4">
<div class="col">
<h4 class="m-0"><strong>@(Title)</strong></h4>
</div>
</div>
@if (_loaded)
{
if (!_items.IsNullOrEmpty())
{
for (int i = 0; i < _items.Count; i++)
{
if (i > 0)
{
<div class="row">
<div class="col">
<hr/>
</div>
</div>
}
<div class="row">
<div class="col">
<a class="text-reset text-decoration-none" href="@(string.Format(UrlIdTemplate, IdSource(_items[i])))">
<ListItemComponent Id="@(IdSource(_items[i]))"
Name="@(NameSource(_items[i]))"
AdditionalNameInfo="@(AdditionalNameInfoSource(_items[i]))"
PictureDownloadingTask="@(PictureDownloadingTask)"
RatingDownloadingTask="@(RatingDownloadingTask)"/>
</a>
</div>
</div>
}
if (!_allItemsLoaded)
{
<div class="row mt-3">
<div class="col">
<div class="d-flex justify-content-center">
<button class="btn btn-secondary" @onclick="DownloadItems">
@if (!_itemsLoading)
{
<span>Load more</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Saving...</span>
}
</button>
</div>
</div>
</div>
}
}
else
{
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
No items found
</div>
</div>
</div>
}
}
else
{
<div class="row">
<div class="col">
<LoadingComponent Color="light"/>
</div>
</div>
}
</div>
</div>

View File

@@ -0,0 +1,93 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model;
using WatchIt.Common.Model.Rating;
using WatchIt.Common.Query;
namespace WatchIt.Website.Components.SearchPage;
public partial class SearchResultComponent<TItem, TQuery> : ComponentBase where TQuery : QueryParameters
{
#region PARAMETERS
[Parameter] public required string Title { get; set; }
[Parameter] public required TQuery Query { get; set; }
[Parameter] public required Func<TItem, long> IdSource { get; set; }
[Parameter] public required Func<TItem, string> NameSource { get; set; }
[Parameter] public Func<TItem, string?> AdditionalNameInfoSource { get; set; } = _ => null;
[Parameter] public required string UrlIdTemplate { get; set; }
[Parameter] public required Func<TQuery, Action<IEnumerable<TItem>>, Task> ItemDownloadingTask { get; set; }
[Parameter] public required Func<long, Action<Picture>, Task> PictureDownloadingTask { get; set; }
[Parameter] public required Func<long, Action<RatingResponse>, Task> RatingDownloadingTask { get; set; }
#endregion
#region FIELDS
private bool _loaded;
private List<TItem> _items = [];
private bool _allItemsLoaded;
private bool _itemsLoading;
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// INIT
Query.First = 5;
List<Task> endTasks = new List<Task>();
// STEP 0
endTasks.AddRange(
[
ItemDownloadingTask(Query, data =>
{
_items.AddRange(data);
if (data.Count() < 5)
{
_allItemsLoaded = true;
}
else
{
Query.After = 5;
}
})
]);
// END
await Task.WhenAll(endTasks);
_loaded = true;
StateHasChanged();
}
}
private async Task DownloadItems()
{
_itemsLoading = true;
await ItemDownloadingTask(Query, data =>
{
_items.AddRange(data);
if (data.Count() < 5)
{
_allItemsLoaded = true;
}
else
{
Query.After += 5;
}
_itemsLoading = false;
});
}
#endregion
}

View File

@@ -1,178 +1,97 @@
@using WatchIt.Common.Model.Photos @using WatchIt.Common.Model.Photos
@using WatchIt.Website.Services.WebAPI.Photos @using WatchIt.Website.Services.WebAPI.Photos
@inherits LayoutComponentBase @inherits LayoutComponentBase
@if (_loaded)
{
<CascadingValue Value="this">
@if (_loaded)
{
<div class="container-xl"> <div class="container-xl">
<div class="row align-items-center m-1 my-2 mb-3 rounded-3 header panel panel-header z-3"> <div class="row sticky-top top-3 mb-2rem">
<div class="col-2"> <div class="col">
<a class="logo" href="/"> <div class="panel panel-header rounded-3 px-3">
<div class="container-grid">
<div class="row align-items-center">
<div class="col">
<a id="logo" class="logo" href="/">
WatchIt WatchIt
</a> </a>
</div> </div>
<div class="col">
<p>Menu</p>
</div>
<div class="col-auto"> <div class="col-auto">
<div class="d-flex flex-row-reverse"> <div class="d-flex gap-2 align-items-center">
@if (_user is null) @if (_searchbarVisible)
{ {
<a class="main-button" href="/auth">Sign in</a> <div class="input-group input-group-sm">
<InputText class="form-control" placeholder="Search with regex" @bind-Value="@(_searchbarText)"/>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="@(SearchStart)">⌕</button>
<a id="searchbarCancel" @onclick="@(() => _searchbarVisible = false)">
<img src="assets/icons/cancel.png" alt="cancel_icon" height="20" width="20"/>
</a>
} }
else else
{ {
<div class="dropdown z-3"> <button type="button" class="btn btn-sm">Rankings</button>
<a class="dropdown-toggle align-items-center text-decoration-none d-flex" id="dropdownUser" aria-expanded="false" @onclick="() => _userMenuIsActive = !_userMenuIsActive"> <button type="button" class="btn btn-sm" @onclick="@(() => _searchbarVisible = true)">⌕</button>
<img class="rounded-circle" alt="avatar" height="30" src="@(_userProfilePicture)"/> }
<div class="text-decoration-none mx-2 text-white">@(_user.Username)</div> </div>
</a> </div>
<ul class="dropdown-menu dropdown-menu-right text-small z-3" id="user-menu" aria-labelledby="dropdownUser"> <div class="col">
<li> <div class="float-end">
@if (_user is null)
{
<a id="signInButton" class="main-button" href="/auth">Sign in</a>
}
else
{
<Dropdown RightAligned>
<Button Color="Color.Default">
<div class="d-flex gap-2 align-items-center">
<img class="rounded-circle" alt="avatar" height="30" src="@(_userProfilePicture is null ? "assets/user_placeholder.png" : _userProfilePicture.ToString())"/>
<span>@(_user.Username)</span>
</div>
</Button>
<DropdownToggle Color="Color.Default" Split />
<DropdownMenu >
@if (_user.IsAdmin) @if (_user.IsAdmin)
{ {
<a class="dropdown-item" href="/admin">Administrator panel</a> <DropdownItem Clicked="@(() => NavigationManager.NavigateTo("/admin"))">Administrator panel</DropdownItem>
}
<DropdownDivider/>
<DropdownItem Clicked="UserMenuLogOut"><span class="text-danger">Log out</span></DropdownItem>
</DropdownMenu>
</Dropdown>
} }
<div class="dropdown-menu-separator"></div>
<a class="dropdown-item text-danger" @onclick="UserMenuLogOut">Log out</a>
</li>
</ul>
</div> </div>
} </div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col z-0 p-1"> <div class="col">
@Body @Body
</div> </div>
</div> </div>
</div> </div>
<style> <style>
html { /* TAGS */
height: 100%;
}
body { body {
background-image: url('@_background'); background-image: url('@(GetBackgroundPhoto() is null ? "assets/background_temp.jpg": GetBackgroundPhoto().ToString())');
height: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
} }
.logo, .main-button {
background-image: linear-gradient(45deg, @_firstGradientColor, @_secondGradientColor);
}
#user-menu { /* IDS */
display: @(_userMenuIsActive ? "block" : "none");
position: fixed; #logo, #signInButton {
background-image: linear-gradient(45deg, @(GetBackgroundPhoto() is null ? "#c6721c, #85200c" : $"#{Convert.ToHexString(GetBackgroundPhoto().Background.FirstGradientColor)}, #{Convert.ToHexString(GetBackgroundPhoto().Background.SecondGradientColor)}"));
} }
</style> </style>
}
@code
{
#region SERVICES
[Inject] public ILogger<MainLayout> Logger { get; set; } = default!;
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
[Inject] public ITokensService TokensService { get; set; } = default!;
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
[Inject] public IAccountsWebAPIService AccountsWebAPIService { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IPhotosWebAPIService PhotosWebAPIService { get; set; } = default!;
#endregion
#region FIELDS
private bool _loaded = false;
private string _background = "assets/background_temp.jpg";
private string _firstGradientColor = "#c6721c";
private string _secondGradientColor = "#85200c";
private User? _user = null;
private string _userProfilePicture = "assets/user_placeholder.png";
private bool _userMenuIsActive = false;
#endregion
#region METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> bgTasks = new List<Task>();
bgTasks.Add(GetBackground());
await GetAuthenticatedUser();
if (_user is not null)
{
bgTasks.Add(GetProfilePicture());
} }
</CascadingValue>
await Task.WhenAll(bgTasks);
_loaded = true;
StateHasChanged();
}
}
private async Task GetBackground()
{
Action<PhotoResponse> backgroundSuccess = (data) =>
{
string imageBase64 = Convert.ToBase64String(data.Image);
string firstColor = BitConverter.ToString(data.Background.FirstGradientColor)
.Replace("-", string.Empty);
string secondColor = BitConverter.ToString(data.Background.SecondGradientColor)
.Replace("-", string.Empty);
_background = $"data:{data.MimeType};base64,{imageBase64}";
_firstGradientColor = $"#{firstColor}";
_secondGradientColor = $"#{secondColor}";
};
await PhotosWebAPIService.GetPhotoRandomBackground(backgroundSuccess);
}
private async Task GetAuthenticatedUser()
{
_user = await AuthenticationService.GetUserAsync();
}
private async Task GetProfilePicture()
{
Action<AccountProfilePictureResponse> successAction = (data) =>
{
string imageBase64 = Convert.ToBase64String(data.Image);
_userProfilePicture = $"data:{data.MimeType};base64,{imageBase64}";
};
await AccountsWebAPIService.GetAccountProfilePicture(_user.Id, successAction);
}
private async Task UserMenuLogOut()
{
await AuthenticationService.LogoutAsync();
await TokensService.RemoveAuthenticationData();
NavigationManager.Refresh(true);
}
#endregion
}

View File

@@ -0,0 +1,140 @@
using System.Net;
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Accounts;
using WatchIt.Common.Model.Photos;
using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.Utility.Tokens;
using WatchIt.Website.Services.WebAPI.Accounts;
using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Photos;
namespace WatchIt.Website.Layout;
public partial class MainLayout : LayoutComponentBase
{
#region SERVICES
[Inject] public ILogger<MainLayout> Logger { get; set; } = default!;
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
[Inject] public ITokensService TokensService { get; set; } = default!;
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
[Inject] public IAccountsWebAPIService AccountsWebAPIService { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IPhotosWebAPIService PhotosWebAPIService { get; set; } = default!;
#endregion
#region FIELDS
private bool _loaded;
private User? _user;
private PhotoResponse? _defaultBackgroundPhoto;
private AccountProfilePictureResponse? _userProfilePicture;
private bool _searchbarVisible;
private string _searchbarText = string.Empty;
#endregion
#region PROPERTIES
private PhotoResponse? _backgroundPhoto;
public PhotoResponse? BackgroundPhoto
{
get => _backgroundPhoto;
set
{
_backgroundPhoto = value;
StateHasChanged();
}
}
#endregion
#region PRIVATE METHODS
#region Main
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> endTasks = new List<Task>();
List<Task> step1Tasks = new List<Task>();
// STEP 0
step1Tasks.AddRange(
[
Task.Run(async () => _user = await AuthenticationService.GetUserAsync())
]);
endTasks.AddRange(
[
PhotosWebAPIService.GetPhotoRandomBackground(data => _defaultBackgroundPhoto = data)
]);
// STEP 1
await Task.WhenAll(step1Tasks);
if (_user is not null)
{
endTasks.AddRange(
[
AccountsWebAPIService.GetAccountProfilePicture(_user.Id, data => _userProfilePicture = data)
]);
}
// END
await Task.WhenAll(endTasks);
_loaded = true;
StateHasChanged();
}
}
private PhotoResponse? GetBackgroundPhoto()
{
if (BackgroundPhoto?.Background is not null)
{
return BackgroundPhoto;
}
else if (_defaultBackgroundPhoto?.Background is not null)
{
return _defaultBackgroundPhoto;
}
return null;
}
#endregion
#region Search
private void SearchStart()
{
if (!string.IsNullOrWhiteSpace(_searchbarText))
{
string query = WebUtility.UrlEncode(_searchbarText);
NavigationManager.NavigateTo($"/search/{query}");
}
}
#endregion
#region User menu
private async Task UserMenuLogOut()
{
await AuthenticationService.LogoutAsync();
await TokensService.RemoveAuthenticationData();
NavigationManager.Refresh(true);
}
#endregion
#endregion
}

View File

@@ -1,21 +1,18 @@
body { /* TAGS */
background-size: cover;
}
.logo {
font-size: 40px;
}
.header {
position: sticky;
top: 10px;
height: 60px;
}
.body-content {
padding-top: 100px;
}
h1 { h1 {
margin: 0; margin: 0;
} }
/* IDS */
#logo {
font-size: 40px;
}
#searchbarCancel {
cursor: pointer;
color: #6c757d;
}

View File

@@ -2,7 +2,7 @@
<PageTitle>WatchIt administrator panel</PageTitle> <PageTitle>WatchIt administrator panel</PageTitle>
<div class="container-fluid"> <div class="container-grid">
@if (_loaded) @if (_loaded)
{ {
if (_authenticated) if (_authenticated)
@@ -26,11 +26,21 @@
} }
else else
{ {
<div class="row">
<div class="col">
<ErrorComponent ErrorMessage="You do not have permission to view this site"/> <ErrorComponent ErrorMessage="You do not have permission to view this site"/>
</div>
</div>
} }
} }
else else
{ {
<div class="row">
<div class="col">
<div class="m-5">
<LoadingComponent/> <LoadingComponent/>
</div>
</div>
</div>
} }
</div> </div>

View File

@@ -1,11 +1,12 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using WatchIt.Website.Layout;
using WatchIt.Website.Services.Utility.Authentication; using WatchIt.Website.Services.Utility.Authentication;
namespace WatchIt.Website.Pages; namespace WatchIt.Website.Pages;
public partial class AdminPage public partial class AdminPage
{ {
#region SERVICE #region SERVICES
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!; [Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
@@ -13,6 +14,14 @@ public partial class AdminPage
#region PARAMETERS
[CascadingParameter] public MainLayout Layout { get; set; }
#endregion
#region FIELDS #region FIELDS
private bool _loaded = false; private bool _loaded = false;
@@ -28,6 +37,8 @@ public partial class AdminPage
{ {
if (firstRender) if (firstRender)
{ {
Layout.BackgroundPhoto = null;
User? user = await AuthenticationService.GetUserAsync(); User? user = await AuthenticationService.GetUserAsync();
if (user is not null && user.IsAdmin) if (user is not null && user.IsAdmin)
{ {

View File

@@ -1,9 +1,8 @@
@page "/" @page "/"
@using WatchIt.Common.Model.Movies
<PageTitle>WatchIt</PageTitle> <PageTitle>WatchIt</PageTitle>
<div class="container-fluid"> <div class="container-grid">
@if (_loaded) @if (_loaded)
{ {
if (string.IsNullOrWhiteSpace(_error)) if (string.IsNullOrWhiteSpace(_error))
@@ -11,7 +10,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="rounded-3 panel panel-regular p-4"> <div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid p-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h4><strong>Top 5 movies this week by popularity</strong></h4> <h4><strong>Top 5 movies this week by popularity</strong></h4>
@@ -26,7 +25,7 @@
<a class="text-reset text-decoration-none" href="/media/@_topMovies.ToArray()[i].Key.Id"> <a class="text-reset text-decoration-none" href="/media/@_topMovies.ToArray()[i].Key.Id">
<div class="d-flex flex-column align-items-center gap-2 h-100"> <div class="d-flex flex-column align-items-center gap-2 h-100">
<img class="rounded-2 shadow object-fit-cover poster-aspect-ratio" src="@(_topMovies.ToArray()[i].Value is not null ? _topMovies.ToArray()[i].Value.ToString() : "assets/poster.png")" alt="poster" width="100%"/> <img class="rounded-2 shadow object-fit-cover poster-aspect-ratio" src="@(_topMovies.ToArray()[i].Value is not null ? _topMovies.ToArray()[i].Value.ToString() : "assets/poster.png")" alt="poster" width="100%"/>
<div class="container-fluid p-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<div class="text-center border border-2 border-light rounded-circle place-circle"><strong>@(i + 1)</strong></div> <div class="text-center border border-2 border-light rounded-circle place-circle"><strong>@(i + 1)</strong></div>
@@ -49,7 +48,7 @@
<div class="row mt-3"> <div class="row mt-3">
<div class="col"> <div class="col">
<div class="rounded-3 panel panel-regular p-4"> <div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid p-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h4><strong>Top 5 TV series this week by popularity</strong></h4> <h4><strong>Top 5 TV series this week by popularity</strong></h4>
@@ -64,7 +63,7 @@
<a class="text-reset text-decoration-none" href="/media/@_topSeries.ToArray()[i].Key.Id"> <a class="text-reset text-decoration-none" href="/media/@_topSeries.ToArray()[i].Key.Id">
<div class="d-flex flex-column align-items-center gap-2 h-100"> <div class="d-flex flex-column align-items-center gap-2 h-100">
<img class="rounded-2 shadow object-fit-cover poster-aspect-ratio" src="@(_topSeries.ToArray()[i].Value is not null ? _topSeries.ToArray()[i].Value.ToString() : "assets/poster.png")" alt="poster" width="100%"/> <img class="rounded-2 shadow object-fit-cover poster-aspect-ratio" src="@(_topSeries.ToArray()[i].Value is not null ? _topSeries.ToArray()[i].Value.ToString() : "assets/poster.png")" alt="poster" width="100%"/>
<div class="container-fluid p-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<div class="text-center border border-2 border-light rounded-circle place-circle"><strong>@(i + 1)</strong></div> <div class="text-center border border-2 border-light rounded-circle place-circle"><strong>@(i + 1)</strong></div>
@@ -87,11 +86,21 @@
} }
else else
{ {
<div class="row">
<div class="col">
<ErrorComponent ErrorMessage="@_error"/> <ErrorComponent ErrorMessage="@_error"/>
</div>
</div>
} }
} }
else else
{ {
<div class="row">
<div class="col">
<div class="m-5">
<LoadingComponent/> <LoadingComponent/>
</div>
</div>
</div>
} }
</div> </div>

View File

@@ -2,6 +2,7 @@
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Movies; using WatchIt.Common.Model.Movies;
using WatchIt.Common.Model.Series; using WatchIt.Common.Model.Series;
using WatchIt.Website.Layout;
using WatchIt.Website.Services.WebAPI.Media; using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies; using WatchIt.Website.Services.WebAPI.Movies;
using WatchIt.Website.Services.WebAPI.Series; using WatchIt.Website.Services.WebAPI.Series;
@@ -21,6 +22,14 @@ public partial class HomePage
#region PARAMETERS
[CascadingParameter] public MainLayout Layout { get; set; }
#endregion
#region FIELDS #region FIELDS
private bool _loaded; private bool _loaded;
@@ -39,6 +48,8 @@ public partial class HomePage
{ {
if (firstRender) if (firstRender)
{ {
Layout.BackgroundPhoto = null;
List<Task> step1Tasks = new List<Task>(); List<Task> step1Tasks = new List<Task>();
List<Task> endTasks = new List<Task>(); List<Task> endTasks = new List<Task>();

View File

@@ -40,7 +40,7 @@
} }
</PageTitle> </PageTitle>
<div class="container-fluid"> <div class="container-grid">
@if (_loaded) @if (_loaded)
{ {
if (string.IsNullOrWhiteSpace(_error)) if (string.IsNullOrWhiteSpace(_error))
@@ -57,7 +57,7 @@
<div class="row mt-3 gx-3"> <div class="row mt-3 gx-3">
<div class="col-auto"> <div class="col-auto">
<div class="rounded-3 panel panel-regular p-4 h-100"> <div class="rounded-3 panel panel-regular p-4 h-100">
<div class="container-fluid p-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<img class="rounded-2 shadow object-fit-cover" src="@(_mediaPosterRequest is not null ? _mediaPosterRequest.ToString() : "assets/poster.png")" alt="poster" width="300" height="500"/> <img class="rounded-2 shadow object-fit-cover" src="@(_mediaPosterRequest is not null ? _mediaPosterRequest.ToString() : "assets/poster.png")" alt="poster" width="300" height="500"/>
@@ -77,12 +77,12 @@
<button type="button" class="btn btn-secondary btn-block btn-stretch-x" @onclick="SavePoster" disabled=@(!Id.HasValue || _mediaPosterSaving || _mediaPosterDeleting) autocomplete="off"> <button type="button" class="btn btn-secondary btn-block btn-stretch-x" @onclick="SavePoster" disabled=@(!Id.HasValue || _mediaPosterSaving || _mediaPosterDeleting) autocomplete="off">
@if (!_mediaPosterSaving) @if (!_mediaPosterSaving)
{ {
<span class="sr-only">Save poster</span> <span>Save poster</span>
} }
else else
{ {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Saving...</span> <span>Saving...</span>
} }
</button> </button>
</div> </div>
@@ -96,12 +96,12 @@
<button type="button" class="btn btn-danger btn-block btn-stretch-x" @onclick="DeletePoster" disabled=@(!Id.HasValue || _mediaPosterSaving || _mediaPosterDeleting) autocomplete="off"> <button type="button" class="btn btn-danger btn-block btn-stretch-x" @onclick="DeletePoster" disabled=@(!Id.HasValue || _mediaPosterSaving || _mediaPosterDeleting) autocomplete="off">
@if (!_mediaPosterSaving) @if (!_mediaPosterSaving)
{ {
<span class="sr-only">Delete poster</span> <span>Delete poster</span>
} }
else else
{ {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Deleting...</span> <span>Deleting...</span>
} }
</button> </button>
</div> </div>
@@ -115,7 +115,7 @@
<div class="rounded-3 panel panel-regular p-4 h-100"> <div class="rounded-3 panel panel-regular p-4 h-100">
<EditForm Model="_mediaRequest"> <EditForm Model="_mediaRequest">
<AntiforgeryToken/> <AntiforgeryToken/>
<div class="container-fluid p-0"> <div class="container-grid">
<div class="row form-group mb-1"> <div class="row form-group mb-1">
<label for="title" class="col-2 col-form-label">Title*</label> <label for="title" class="col-2 col-form-label">Title*</label>
<div class="col-10"> <div class="col-10">
@@ -183,12 +183,12 @@
<button type="button" class="btn btn-secondary" @onclick="SaveBasicData"> <button type="button" class="btn btn-secondary" @onclick="SaveBasicData">
@if (!_basicDataSaving) @if (!_basicDataSaving)
{ {
<span class="sr-only">Save</span> <span>Save</span>
} }
else else
{ {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Saving...</span> <span>Saving...</span>
} }
</button> </button>
</div> </div>
@@ -202,7 +202,7 @@
<div class="row mt-3"> <div class="row mt-3">
<div class="col"> <div class="col">
<div class="rounded-3 panel panel-regular p-4"> <div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid p-0"> <div class="container-grid">
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<div class="d-flex align-items-center h-100"> <div class="d-flex align-items-center h-100">
@@ -226,12 +226,12 @@
<button type="button" class="btn btn-secondary" disabled="@(_photoEditSaving)" @onclick="SaveEditPhoto"> <button type="button" class="btn btn-secondary" disabled="@(_photoEditSaving)" @onclick="SaveEditPhoto">
@if (!_photoEditSaving) @if (!_photoEditSaving)
{ {
<span class="sr-only">Save</span> <span>Save</span>
} }
else else
{ {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Saving...</span> <span>Saving...</span>
} }
</button> </button>
<button type="button" class="btn btn-secondary" disabled="@(_photoEditSaving)" @onclick="CancelEditPhoto">Cancel</button> <button type="button" class="btn btn-secondary" disabled="@(_photoEditSaving)" @onclick="CancelEditPhoto">Cancel</button>
@@ -248,7 +248,7 @@
<div id="scrollPhotos" class="d-flex p-3 gap-3" data-bs-spy="scroll" tabindex="0"> <div id="scrollPhotos" class="d-flex p-3 gap-3" data-bs-spy="scroll" tabindex="0">
@foreach (PhotoResponse photo in _photos) @foreach (PhotoResponse photo in _photos)
{ {
<div class="container-fluid p-0 m-0 photo-container"> <div class="container-grid photo-container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<img class="rounded-1 shadow object-fit-cover photo-default-aspect-ratio" src="@(photo.ToString())" alt="photo" width="350"/> <img class="rounded-1 shadow object-fit-cover photo-default-aspect-ratio" src="@(photo.ToString())" alt="photo" width="350"/>
@@ -301,10 +301,10 @@
} }
else else
{ {
<div class="container-fluid p-0 m-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<div class="container-fluid p-0 m-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col"> <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"/> <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"/>
@@ -321,7 +321,7 @@
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="container-fluid p-0 m-0"> <div class="container-grid">
<div class="row form-group"> <div class="row form-group">
<div class="col"> <div class="col">
<div class="form-check"> <div class="form-check">
@@ -362,31 +362,30 @@
} }
else else
{ {
<div class="row">
<div class="col">
<ErrorComponent ErrorMessage="You do not have permission to view this site"/> <ErrorComponent ErrorMessage="You do not have permission to view this site"/>
</div>
</div>
} }
} }
else else
{ {
<div class="row">
<div class="col">
<ErrorComponent ErrorMessage="@_error"/> <ErrorComponent ErrorMessage="@_error"/>
</div>
</div>
} }
} }
else else
{ {
<div class="row">
<div class="col">
<div class="m-5">
<LoadingComponent/> <LoadingComponent/>
</div>
</div>
</div>
} }
</div> </div>
@if (_background is not null)
{
<style>
body {
background-image: url('@(_background.ToString())') !important;
}
.logo, .main-button {
background-image: linear-gradient(45deg, @($"#{BitConverter.ToString(_background.Background.FirstGradientColor).Replace("-", string.Empty)}"), @($"#{BitConverter.ToString(_background.Background.SecondGradientColor).Replace("-", string.Empty)}")) !important;
}
</style>
}

View File

@@ -5,6 +5,7 @@ using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Movies; using WatchIt.Common.Model.Movies;
using WatchIt.Common.Model.Photos; using WatchIt.Common.Model.Photos;
using WatchIt.Common.Model.Series; using WatchIt.Common.Model.Series;
using WatchIt.Website.Layout;
using WatchIt.Website.Services.Utility.Authentication; using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.WebAPI.Media; using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies; using WatchIt.Website.Services.WebAPI.Movies;
@@ -33,6 +34,8 @@ public partial class MediaEditPage : ComponentBase
[Parameter] public long? Id { get; set; } [Parameter] public long? Id { get; set; }
[Parameter] public string? Type { get; set; } [Parameter] public string? Type { get; set; }
[CascadingParameter] public MainLayout Layout { get; set; }
#endregion #endregion
@@ -44,8 +47,6 @@ public partial class MediaEditPage : ComponentBase
private User? _user; private User? _user;
private PhotoResponse? _background;
private MediaResponse? _media; private MediaResponse? _media;
private MovieRequest? _movieRequest; private MovieRequest? _movieRequest;
private SeriesRequest? _seriesRequest; private SeriesRequest? _seriesRequest;
@@ -86,6 +87,8 @@ public partial class MediaEditPage : ComponentBase
{ {
if (firstRender) if (firstRender)
{ {
Layout.BackgroundPhoto = null;
List<Task> step1Tasks = new List<Task>(); List<Task> step1Tasks = new List<Task>();
List<Task> step2Tasks = new List<Task>(); List<Task> step2Tasks = new List<Task>();
List<Task> endTasks = new List<Task>(); List<Task> endTasks = new List<Task>();
@@ -112,7 +115,7 @@ public partial class MediaEditPage : ComponentBase
{ {
endTasks.AddRange( endTasks.AddRange(
[ [
MediaWebAPIService.GetMediaPhotoRandomBackground(Id.Value, data => _background = data), MediaWebAPIService.GetMediaPhotoRandomBackground(Id.Value, data => Layout.BackgroundPhoto = data),
MediaWebAPIService.GetMediaPoster(Id.Value, data => MediaWebAPIService.GetMediaPoster(Id.Value, data =>
{ {
_mediaPosterSaved = data; _mediaPosterSaved = data;

View File

@@ -24,7 +24,7 @@ else
<div class="container-fluid"> <div class="container-grid">
@if (_loaded) @if (_loaded)
{ {
if (string.IsNullOrWhiteSpace(_error)) if (string.IsNullOrWhiteSpace(_error))
@@ -35,7 +35,7 @@ else
</div> </div>
<div class="col"> <div class="col">
<div class="d-flex h-100"> <div class="d-flex h-100">
<div class="container-fluid px-0 align-self-end"> <div class="container-grid align-self-end">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1 class="align-self-end title-shadow"> <h1 class="align-self-end title-shadow">
@@ -60,7 +60,7 @@ else
<div class="row mt-3 gx-3"> <div class="row mt-3 gx-3">
<div class="col"> <div class="col">
<div class="rounded-3 panel panel-regular p-4 h-100"> <div class="rounded-3 panel panel-regular p-4 h-100">
<div class="container-fluid px-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="d-flex flex-wrap gap-3"> <div class="d-flex flex-wrap gap-3">
@@ -133,7 +133,7 @@ else
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class="rounded-3 panel panel-yellow p-4 h-100"> <div class="rounded-3 panel panel-yellow p-4 h-100">
<div class="container-fluid px-0"> <div class="container-grid">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h4 class="text-dark"> <h4 class="text-dark">
@@ -193,26 +193,21 @@ else
} }
else else
{ {
<div class="row">
<div class="col">
<ErrorComponent ErrorMessage="@_error"/> <ErrorComponent ErrorMessage="@_error"/>
</div>
</div>
} }
} }
else else
{ {
<div class="row">
<div class="col">
<div class="m-5">
<LoadingComponent/> <LoadingComponent/>
</div>
</div>
</div>
} }
</div> </div>
@if (_background is not null)
{
<style>
body {
background-image: url('@($"data:{_background.MimeType};base64,{Convert.ToBase64String(_background.Image)}")') !important;
}
.logo, .main-button {
background-image: linear-gradient(45deg, @($"#{BitConverter.ToString(_background.Background.FirstGradientColor).Replace("-", string.Empty)}"), @($"#{BitConverter.ToString(_background.Background.SecondGradientColor).Replace("-", string.Empty)}")) !important;
}
</style>
}

View File

@@ -4,7 +4,9 @@ using WatchIt.Common.Model.Genres;
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Movies; using WatchIt.Common.Model.Movies;
using WatchIt.Common.Model.Photos; using WatchIt.Common.Model.Photos;
using WatchIt.Common.Model.Rating;
using WatchIt.Common.Model.Series; using WatchIt.Common.Model.Series;
using WatchIt.Website.Layout;
using WatchIt.Website.Services.Utility.Authentication; using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.WebAPI.Media; using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies; using WatchIt.Website.Services.WebAPI.Movies;
@@ -30,6 +32,8 @@ public partial class MediaPage : ComponentBase
[Parameter] public long Id { get; set; } [Parameter] public long Id { get; set; }
[CascadingParameter] public MainLayout Layout { get; set; }
#endregion #endregion
@@ -43,10 +47,9 @@ public partial class MediaPage : ComponentBase
private User? _user; private User? _user;
private PhotoResponse? _background;
private MediaPosterResponse? _poster; private MediaPosterResponse? _poster;
private IEnumerable<GenreResponse> _genres; private IEnumerable<GenreResponse> _genres;
private MediaRatingResponse _globalRating; private RatingResponse _globalRating;
private MovieResponse? _movie; private MovieResponse? _movie;
private SeriesResponse? _series; private SeriesResponse? _series;
@@ -62,6 +65,8 @@ public partial class MediaPage : ComponentBase
{ {
if (firstRender) if (firstRender)
{ {
Layout.BackgroundPhoto = null;
List<Task> step1Tasks = new List<Task>(); List<Task> step1Tasks = new List<Task>();
List<Task> step2Tasks = new List<Task>(); List<Task> step2Tasks = new List<Task>();
List<Task> endTasks = new List<Task>(); List<Task> endTasks = new List<Task>();
@@ -84,7 +89,7 @@ public partial class MediaPage : ComponentBase
endTasks.AddRange( endTasks.AddRange(
[ [
MediaWebAPIService.PostMediaView(Id), MediaWebAPIService.PostMediaView(Id),
MediaWebAPIService.GetMediaPhotoRandomBackground(Id, data => _background = data), MediaWebAPIService.GetMediaPhotoRandomBackground(Id, data => Layout.BackgroundPhoto = data),
MediaWebAPIService.GetMediaPoster(Id, data => _poster = data), MediaWebAPIService.GetMediaPoster(Id, data => _poster = data),
MediaWebAPIService.GetMediaGenres(Id, data => _genres = data), MediaWebAPIService.GetMediaGenres(Id, data => _genres = data),
MediaWebAPIService.GetMediaRating(Id, data => _globalRating = data), MediaWebAPIService.GetMediaRating(Id, data => _globalRating = data),
@@ -119,7 +124,7 @@ public partial class MediaPage : ComponentBase
} }
else else
{ {
await MediaWebAPIService.PutMediaRating(Id, new MediaRatingRequest(rating)); await MediaWebAPIService.PutMediaRating(Id, new RatingRequest(rating));
_userRating = rating; _userRating = rating;
} }
await MediaWebAPIService.GetMediaRating(Id, data => _globalRating = data); await MediaWebAPIService.GetMediaRating(Id, data => _globalRating = data);

View File

@@ -0,0 +1,79 @@
@using WatchIt.Common.Model.Movies
@using WatchIt.Common.Model.Series
@using WatchIt.Common.Query
@using WatchIt.Website.Components.SearchPage
@using WatchIt.Website.Services.WebAPI.Movies
@layout MainLayout
@page "/search/{query}"
<PageTitle>WatchIt - Searching "@(Query)"</PageTitle>
<div class="container-grid">
@if (_loaded)
{
if (string.IsNullOrWhiteSpace(_error))
{
<div class="row">
<div class="col">
<div class="rounded-3 panel panel-regular p-3">
<div class="d-flex justify-content-center">
<h3 class="m-0">
<strong>Search results for phrase:</strong> "@(DecodedQuery)"
</h3>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col">
<SearchResultComponent TItem="MovieResponse"
TQuery="MovieQueryParameters"
Title="Movies"
UrlIdTemplate="/media/{0}"
IdSource="@(item => item.Id)"
NameSource="@(item => item.Title)"
AdditionalNameInfoSource="@(item => item.ReleaseDate.HasValue ? $" ({item.ReleaseDate.Value.Year})" : null)"
Query="@(new MovieQueryParameters { Title = DecodedQuery })"
ItemDownloadingTask="@(MoviesWebAPIService.GetAllMovies)"
PictureDownloadingTask="@((id, action) => MediaWebAPIService.GetMediaPoster(id, action))"
RatingDownloadingTask="@((id, action) => MediaWebAPIService.GetMediaRating(id, action))"/>
</div>
</div>
<div class="row mt-3">
<div class="col">
<SearchResultComponent TItem="SeriesResponse"
TQuery="SeriesQueryParameters"
Title="TV series"
UrlIdTemplate="/media/{0}"
IdSource="@(item => item.Id)"
NameSource="@(item => item.Title)"
AdditionalNameInfoSource="@(item => item.ReleaseDate.HasValue ? $" ({item.ReleaseDate.Value.Year})" : null)"
Query="@(new SeriesQueryParameters { Title = DecodedQuery })"
ItemDownloadingTask="@(SeriesWebAPIService.GetAllSeries)"
PictureDownloadingTask="@((id, action) => MediaWebAPIService.GetMediaPoster(id, action))"
RatingDownloadingTask="@((id, action) => MediaWebAPIService.GetMediaRating(id, action))"/>
</div>
</div>
}
else
{
<div class="row">
<div class="col">
<ErrorComponent ErrorMessage="@(_error)"/>
</div>
</div>
}
}
else
{
<div class="row">
<div class="col">
<LoadingComponent/>
</div>
</div>
}
</div>

View File

@@ -0,0 +1,61 @@
using System.Net;
using Microsoft.AspNetCore.Components;
using WatchIt.Website.Layout;
using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies;
using WatchIt.Website.Services.WebAPI.Series;
namespace WatchIt.Website.Pages;
public partial class SearchPage : ComponentBase
{
#region SERVICES
[Inject] private IMoviesWebAPIService MoviesWebAPIService { get; set; } = default!;
[Inject] private ISeriesWebAPIService SeriesWebAPIService { get; set; } = default!;
[Inject] private IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
#endregion
#region FIELDS
private bool _loaded;
private string? _error;
#endregion
#region PARAMETERS
[Parameter] public string Query { get; set; }
[CascadingParameter] public MainLayout Layout { get; set; }
#endregion
#region PROPERTIES
public string DecodedQuery => WebUtility.UrlDecode(Query);
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_loaded = true;
StateHasChanged();
}
}
#endregion
}

View File

@@ -1,5 +1,8 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Blazorise;
using Blazorise.Bootstrap5;
using Blazorise.Icons.FontAwesome;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using WatchIt.Common.Services.HttpClient; using WatchIt.Common.Services.HttpClient;
using WatchIt.Website.Services.Utility.Authentication; using WatchIt.Website.Services.Utility.Authentication;
@@ -53,6 +56,12 @@ public static class Program
private static WebApplicationBuilder SetupServices(this WebApplicationBuilder builder) private static WebApplicationBuilder SetupServices(this WebApplicationBuilder builder)
{ {
builder.Services.AddSingleton<HttpClient>(); builder.Services.AddSingleton<HttpClient>();
builder.Services.AddBlazorise(options =>
{
options.Immediate = true;
})
.AddBootstrap5Providers()
.AddFontAwesomeIcons();
// Utility // Utility
builder.Services.AddSingleton<IHttpClientService, HttpClientService>(); builder.Services.AddSingleton<IHttpClientService, HttpClientService>();

View File

@@ -35,10 +35,16 @@
<_ContentIncludedByDefault Remove="Components\Pages\Weather.razor" /> <_ContentIncludedByDefault Remove="Components\Pages\Weather.razor" />
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css" /> <_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css.map" /> <_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\scripts\popper.min.js" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="Layout\MainLayout.razor" /> <AdditionalFiles Include="Layout\MainLayout.razor" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="1.6.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.6.1" />
</ItemGroup>
</Project> </Project>

View File

@@ -15,3 +15,4 @@
@using WatchIt.Website.Services.Utility.Authentication @using WatchIt.Website.Services.Utility.Authentication
@using WatchIt.Website.Services.WebAPI.Accounts @using WatchIt.Website.Services.WebAPI.Accounts
@using WatchIt.Website.Services.WebAPI.Media @using WatchIt.Website.Services.WebAPI.Media
@using Blazorise

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

View File

@@ -0,0 +1,99 @@
/* TAGS */
html {
height: 100%;
}
body {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
height: 100%;
}
body, html {
background-color: transparent;
height: 100%;
margin: 0;
padding: 0;
color: lightgray;
font-family: "PT Sans";
}
/* CLASSES */
.container-grid {
padding: 0 !important;
margin: 0 !important;
--bs-gutter-x: 1.5rem;
--bs-gutter-y: 0;
width: 100%;
}
.logo {
font-family: "Belanosima";
text-decoration: none;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.top-3 {
top: 1rem !important;
}
.mb-2rem {
margin-bottom: 2rem !important;
}
.mt-9 {
margin-top: 9rem !important;
}
.panel-header {
background-color: rgba(0, 0, 0, 0.8);
}
.panel-regular {
background-color: rgba(0, 0, 0, 0.6);
}
.panel-yellow {
background-color: rgba(255, 184, 58, 0.6);
}
.panel {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(25px);
}
.dropdown-menu-left {
right: auto;
left: 0;
}
.btn-stretch-x {
width: 100%;
}
.w-100 {
width: 100%;
}
.picture-aspect-ratio {
aspect-ratio: 3/5;
}

View File

@@ -1,40 +1,4 @@
body, html { /* CLASSES */
background-color: transparent;
height: 100%;
margin: 0;
padding: 0;
color: lightgray;
font-family: "PT Sans";
}
.logo {
font-family: "Belanosima";
text-decoration: none;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.mt-9 {
margin-top: 9rem !important;
}
.panel-header {
background-color: rgba(0, 0, 0, 0.8);
}
.panel-regular {
background-color: rgba(0, 0, 0, 0.6);
}
.panel-yellow {
background-color: rgba(255, 184, 58, 0.6);
}
.panel {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(25px);
}
.main-button { .main-button {
--r:10px; --r:10px;
@@ -84,16 +48,3 @@ body, html {
.main-button:hover::before { .main-button:hover::before {
-webkit-mask:none; -webkit-mask:none;
} }
.dropdown-menu-left {
right: auto;
left: 0;
}
.btn-stretch-x {
width: 100%;
}
.w-100 {
width: 100%;
}