Merge pull request #90 from mateuszskoczek/dev/0.2.0

Dev/0.2.0
This commit is contained in:
2024-10-02 01:16:37 +02:00
committed by GitHub
Unverified
101 changed files with 3170 additions and 967 deletions

View File

@@ -1,4 +1,4 @@
next-version: 0.1.0 next-version: 0.2.0
assembly-versioning-scheme: MajorMinorPatch assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch assembly-file-versioning-scheme: MajorMinorPatch

View File

@@ -1,12 +1,15 @@
name: "[Pull request][dev] Build and test" name: "Pull request to 'dev' branch"
on: on:
pull_request: pull_request:
branches: [ "dev" ] branches:
- "dev/**"
paths:
- 'WatchIt**'
jobs: jobs:
build: build:
name: Build and test name: Dotnet solution build test and unit tests execution
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

26
.github/workflows/dev_push.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: "Push to 'dev' branch"
on:
push:
branches:
- "dev/**"
paths:
- 'WatchIt**'
jobs:
build:
name: Dotnet solution build test and unit tests execution
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal

45
.github/workflows/master_pr.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: "Pull request to 'master' branch"
on:
pull_request:
branches:
- "master"
jobs:
build-dotnet:
name: Dotnet solution build test and unit tests execution
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build solution
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
build-docker:
name: Docker image build test
needs: build-dotnet
runs-on: ubuntu-latest
strategy:
matrix:
app:
- WatchIt.WebAPI
- WatchIt.Website
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Convert name to lowercase
id: name
uses: ASzc/change-string-case-action@v1
with:
string: ${{ matrix.app }}
- name: Build image
run: docker build ${{ github.workspace }} -t ghcr.io/${{github.actor}}/${{ steps.name.outputs.lowercase }}:build-test -f ${{ github.workspace }}/${{ matrix.app }}/${{ matrix.app }}/Dockerfile

View File

@@ -6,26 +6,9 @@ on:
- "master" - "master"
jobs: jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
publish: publish:
name: Publish name: Publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build
strategy: strategy:
matrix: matrix:
app: app:

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

@@ -21,9 +21,9 @@ public class GenreQueryParameters : QueryParameters<GenreResponse>
public override bool IsMeetingConditions(GenreResponse item) => public override bool IsMeetingConditions(GenreResponse item) =>
( (
TestString(item.Name, Name) TestStringWithRegex(item.Name, Name)
&& &&
TestString(item.Description, Description) TestStringWithRegex(item.Description, Description)
); );
#endregion #endregion

View File

@@ -1,12 +1,22 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using WatchIt.Common.Query;
namespace WatchIt.Common.Model.Genres; namespace WatchIt.Common.Model.Genres;
public class GenreResponse : Genre public class GenreResponse : Genre, IQueryOrderable<GenreResponse>
{ {
#region PROPERTIES #region PROPERTIES
[JsonIgnore]
public static IDictionary<string, Func<GenreResponse, IComparable>> OrderableProperties { get; } = new Dictionary<string, Func<GenreResponse, IComparable>>
{
{ "id", x => x.Id },
{ "name", x => x.Name },
{ "description", x => x.Description }
};
[JsonPropertyName("id")] [JsonPropertyName("id")]
public long Id { get; set; } public long Id { get; set; }

View File

@@ -1,18 +0,0 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media;
public class MediaPhoto
{
[JsonPropertyName("media_id")]
public required long MediaId { get; set; }
[JsonPropertyName("image")]
public required byte[] Image { get; set; }
[JsonPropertyName("mime_type")]
public required string MimeType { get; set; }
[JsonPropertyName("background")]
public MediaPhotoBackground? Background { get; set; }
}

View File

@@ -1,36 +0,0 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Query;
namespace WatchIt.Common.Model.Media;
public class MediaPhotoQueryParameters : QueryParameters<MediaPhotoResponse>
{
#region PROPERTIES
[FromQuery(Name = "mime_type")]
public string? MimeType { get; set; }
[FromQuery(Name = "is_background")]
public bool? IsBackground { get; set; }
[FromQuery(Name = "is_universal_background")]
public bool? IsUniversalBackground { get; set; }
#endregion
#region PUBLIC METHODS
public override bool IsMeetingConditions(MediaPhotoResponse item) =>
(
TestString(item.MimeType, MimeType)
&&
TestBoolean(item.Background is not null, IsBackground)
&&
TestBoolean(item.Background!.IsUniversalBackground, IsUniversalBackground)
);
#endregion
}

View File

@@ -1,12 +1,31 @@
using WatchIt.Database.Model.Media; using System.Diagnostics.CodeAnalysis;
using WatchIt.Common.Model.Photos;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Media;
public class MediaPhotoRequest : MediaPhoto public class MediaPhotoRequest : Photo
{ {
public MediaPhotoImage CreateMediaPhotoImage() => new MediaPhotoImage #region CONSTRUCTORS
public MediaPhotoRequest() {}
[SetsRequiredMembers]
public MediaPhotoRequest(PhotoResponse response)
{ {
MediaId = MediaId, Image = response.Image;
MimeType = response.MimeType;
}
#endregion
#region PUBLIC METHODS
public MediaPhotoImage CreateMediaPhotoImage(long mediaId) => new MediaPhotoImage
{
MediaId = mediaId,
Image = Image, Image = Image,
MimeType = MimeType MimeType = MimeType
}; };
@@ -18,22 +37,6 @@ public class MediaPhotoRequest : MediaPhoto
FirstGradientColor = Background.FirstGradientColor, FirstGradientColor = Background.FirstGradientColor,
SecondGradientColor = Background.SecondGradientColor SecondGradientColor = Background.SecondGradientColor
}; };
public void UpdateMediaPhotoImage(MediaPhotoImage item)
{
item.MediaId = MediaId;
item.Image = Image;
item.MimeType = MimeType;
item.UploadDate = DateTime.Now;
}
public void UpdateMediaPhotoImageBackground(MediaPhotoImageBackground item) #endregion
{
if (Background is not null)
{
item.IsUniversalBackground = Background.IsUniversalBackground;
item.FirstGradientColor = Background.FirstGradientColor;
item.SecondGradientColor = Background.SecondGradientColor;
}
}
} }

View File

@@ -2,23 +2,6 @@
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Media;
public abstract class MediaPoster public abstract class MediaPoster : Picture
{ {
#region PROPERTIES
[JsonPropertyName("image")]
public required byte[] Image { get; set; }
[JsonPropertyName("mime_type")]
public required string MimeType { get; set; }
#endregion
#region PUBLIC METHODS
public override string ToString() => $"data:{MimeType};base64,{Convert.ToBase64String(Image)}";
#endregion
} }

View File

@@ -0,0 +1,84 @@
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Query;
namespace WatchIt.Common.Model.Media;
public class MediaQueryParameters : QueryParameters<MediaResponse>
{
#region PROPERTIES
[FromQuery(Name = "type")]
public MediaType? Type { get; set; }
[FromQuery(Name = "title")]
public string? Title { get; set; }
[FromQuery(Name = "original_title")]
public string? OriginalTitle { get; set; }
[FromQuery(Name = "description")]
public string? Description { get; set; }
[FromQuery(Name = "release_date")]
public DateOnly? ReleaseDate { get; set; }
[FromQuery(Name = "release_date_from")]
public DateOnly? ReleaseDateFrom { get; set; }
[FromQuery(Name = "release_date_to")]
public DateOnly? ReleaseDateTo { get; set; }
[FromQuery(Name = "length")]
public short? Length { get; set; }
[FromQuery(Name = "length_from")]
public short? LengthFrom { get; set; }
[FromQuery(Name = "length_to")]
public short? LengthTo { get; set; }
[FromQuery(Name = "rating_average")]
public decimal? RatingAverage { get; set; }
[FromQuery(Name = "rating_average_from")]
public decimal? RatingAverageFrom { get; set; }
[FromQuery(Name = "rating_average_to")]
public decimal? RatingAverageTo { get; set; }
[FromQuery(Name = "rating_count")]
public long? RatingCount { get; set; }
[FromQuery(Name = "rating_count_from")]
public long? RatingCountFrom { get; set; }
[FromQuery(Name = "rating_count_to")]
public long? RatingCountTo { get; set; }
#endregion
#region PUBLIC METHODS
public override bool IsMeetingConditions(MediaResponse item) =>
(
Test(item.Type, Type)
&&
TestStringWithRegex(item.Title, Title)
&&
TestStringWithRegex(item.OriginalTitle, OriginalTitle)
&&
TestStringWithRegex(item.Description, Description)
&&
TestComparable(item.ReleaseDate, ReleaseDate, ReleaseDateFrom, ReleaseDateTo)
&&
TestComparable(item.Length, Length, LengthFrom, LengthTo)
&&
TestComparable(item.Rating.Average, RatingAverage, RatingAverageFrom, RatingAverageTo)
&&
TestComparable(item.Rating.Count, RatingCount, RatingCountFrom, RatingCountTo)
);
#endregion
}

View File

@@ -1,33 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media;
public class MediaRatingResponse
{
#region PROPERTIES
[JsonPropertyName("rating_average")]
public required double RatingAverage { get; set; }
[JsonPropertyName("rating_count")]
public required long RatingCount { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public MediaRatingResponse() {}
[SetsRequiredMembers]
public MediaRatingResponse(double ratingAverage, long ratingCount)
{
RatingAverage = ratingAverage;
RatingCount = ratingCount;
}
#endregion
}

View File

@@ -1,17 +1,36 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using WatchIt.Common.Model.Rating;
using WatchIt.Common.Query;
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Media;
public class MediaResponse : Media public class MediaResponse : Media, IQueryOrderable<MediaResponse>
{ {
#region PROPERTIES #region PROPERTIES
[JsonIgnore]
public static IDictionary<string, Func<MediaResponse, IComparable>> OrderableProperties { get; } = new Dictionary<string, Func<MediaResponse, IComparable>>
{
{ "id", x => x.Id },
{ "title", x => x.Title },
{ "original_title", x => x.OriginalTitle },
{ "description", x => x.Description },
{ "release_date", x => x.ReleaseDate },
{ "length", x => x.Length },
{ "rating.average", x => x.Rating.Average },
{ "rating.count", x => x.Rating.Count }
};
[JsonPropertyName("id")] [JsonPropertyName("id")]
public long Id { get; set; } public long Id { get; set; }
[JsonPropertyName("type")] [JsonPropertyName("type")]
public MediaType Type { get; set; } public MediaType Type { get; set; }
[JsonPropertyName("rating")]
public RatingResponse Rating { get; set; }
#endregion #endregion
@@ -32,6 +51,7 @@ public class MediaResponse : Media
ReleaseDate = media.ReleaseDate; ReleaseDate = media.ReleaseDate;
Length = media.Length; Length = media.Length;
Type = mediaType; Type = mediaType;
Rating = new RatingResponse(media.RatingMedia);
} }
#endregion #endregion

View File

@@ -43,6 +43,24 @@ public class MovieQueryParameters : QueryParameters<MovieResponse>
[FromQuery(Name = "budget_to")] [FromQuery(Name = "budget_to")]
public decimal? BudgetTo { get; set; } public decimal? BudgetTo { get; set; }
[FromQuery(Name = "rating_average")]
public decimal? RatingAverage { get; set; }
[FromQuery(Name = "rating_average_from")]
public decimal? RatingAverageFrom { get; set; }
[FromQuery(Name = "rating_average_to")]
public decimal? RatingAverageTo { get; set; }
[FromQuery(Name = "rating_count")]
public long? RatingCount { get; set; }
[FromQuery(Name = "rating_count_from")]
public long? RatingCountFrom { get; set; }
[FromQuery(Name = "rating_count_to")]
public long? RatingCountTo { get; set; }
#endregion #endregion
@@ -51,17 +69,21 @@ public class MovieQueryParameters : QueryParameters<MovieResponse>
public override bool IsMeetingConditions(MovieResponse item) => public override bool IsMeetingConditions(MovieResponse item) =>
( (
TestString(item.Title, Title) TestStringWithRegex(item.Title, Title)
&& &&
TestString(item.OriginalTitle, OriginalTitle) TestStringWithRegex(item.OriginalTitle, OriginalTitle)
&& &&
TestString(item.Description, Description) TestStringWithRegex(item.Description, Description)
&& &&
TestComparable(item.ReleaseDate, ReleaseDate, ReleaseDateFrom, ReleaseDateTo) TestComparable(item.ReleaseDate, ReleaseDate, ReleaseDateFrom, ReleaseDateTo)
&& &&
TestComparable(item.Length, Length, LengthFrom, LengthTo) TestComparable(item.Length, Length, LengthFrom, LengthTo)
&& &&
TestComparable(item.Budget, Budget, BudgetFrom, BudgetTo) TestComparable(item.Budget, Budget, BudgetFrom, BudgetTo)
&&
TestComparable(item.Rating.Average, RatingAverage, RatingAverageFrom, RatingAverageTo)
&&
TestComparable(item.Rating.Count, RatingCount, RatingCountFrom, RatingCountTo)
); );
#endregion #endregion

View File

@@ -1,15 +1,35 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using WatchIt.Common.Model.Rating;
using WatchIt.Common.Query;
using WatchIt.Database.Model.Media; using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Movies; namespace WatchIt.Common.Model.Movies;
public class MovieResponse : Movie public class MovieResponse : Movie, IQueryOrderable<MovieResponse>
{ {
#region PROPERTIES #region PROPERTIES
[JsonIgnore]
public static IDictionary<string, Func<MovieResponse, IComparable>> OrderableProperties { get; } = new Dictionary<string, Func<MovieResponse, IComparable>>
{
{ "id", x => x.Id },
{ "title", x => x.Title },
{ "original_title", x => x.OriginalTitle },
{ "description", x => x.Description },
{ "release_date", x => x.ReleaseDate },
{ "length", x => x.Length },
{ "budget", x => x.Budget },
{ "rating.average", x => x.Rating.Average },
{ "rating.count", x => x.Rating.Count }
};
[JsonPropertyName("id")] [JsonPropertyName("id")]
public long Id { get; set; } public long Id { get; set; }
[JsonPropertyName("rating")]
public RatingResponse Rating { get; set; }
#endregion #endregion
@@ -30,6 +50,7 @@ public class MovieResponse : Movie
ReleaseDate = mediaMovie.Media.ReleaseDate; ReleaseDate = mediaMovie.Media.ReleaseDate;
Length = mediaMovie.Media.Length; Length = mediaMovie.Media.Length;
Budget = mediaMovie.Budget; Budget = mediaMovie.Budget;
Rating = new RatingResponse(mediaMovie.Media.RatingMedia);
} }
#endregion #endregion

View File

@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Photos;
public abstract class Photo : Picture
{
#region PROPERTIES
[JsonPropertyName("background")]
public PhotoBackgroundData? Background { get; set; }
#endregion
}

View File

@@ -1,9 +1,11 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Photos;
public class MediaPhotoBackground public class PhotoBackgroundData
{ {
#region PROPERTIES
[JsonPropertyName("is_universal_background")] [JsonPropertyName("is_universal_background")]
public required bool IsUniversalBackground { get; set; } public required bool IsUniversalBackground { get; set; }
@@ -12,4 +14,6 @@ public class MediaPhotoBackground
[JsonPropertyName("second_gradient_color")] [JsonPropertyName("second_gradient_color")]
public required byte[] SecondGradientColor { get; set; } public required byte[] SecondGradientColor { get; set; }
#endregion
} }

View File

@@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Photos;
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
public MediaPhotoImageBackground CreateMediaPhotoImageBackground(Guid photoId) => new MediaPhotoImageBackground
{
Id = photoId,
IsUniversalBackground = IsUniversalBackground,
FirstGradientColor = FirstGradientColor,
SecondGradientColor = SecondGradientColor,
};
public void UpdateMediaPhotoImageBackground(MediaPhotoImageBackground image)
{
image.IsUniversalBackground = IsUniversalBackground;
image.FirstGradientColor = FirstGradientColor;
image.SecondGradientColor = SecondGradientColor;
}
#endregion
}

View File

@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Series;
using WatchIt.Common.Query;
namespace WatchIt.Common.Model.Photos;
public class PhotoQueryParameters : QueryParameters<PhotoResponse>
{
#region PROPERTIES
[FromQuery(Name = "mime_type")]
public string? MimeType { get; set; }
[FromQuery(Name = "is_background")]
public bool? IsBackground { get; set; }
[FromQuery(Name = "is_universal_background")]
public bool? IsUniversalBackground { get; set; }
[FromQuery(Name = "upload_date")]
public DateOnly? UploadDate { get; set; }
[FromQuery(Name = "upload_date_from")]
public DateOnly? UploadDateFrom { get; set; }
[FromQuery(Name = "upload_date_to")]
public DateOnly? UploadDateTo { get; set; }
#endregion
#region PUBLIC METHODS
public override bool IsMeetingConditions(PhotoResponse item) =>
(
TestStringWithRegex(item.MimeType, MimeType)
&&
Test(item.Background is not null, IsBackground)
&&
Test(item.Background is not null && item.Background.IsUniversalBackground, IsUniversalBackground)
&&
TestComparable(item.UploadDate, UploadDate, UploadDateFrom, UploadDateTo)
);
#endregion
}

View File

@@ -1,16 +1,31 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using WatchIt.Common.Query;
using WatchIt.Database.Model.Media; using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Media; namespace WatchIt.Common.Model.Photos;
public class MediaPhotoResponse : MediaPhoto public class PhotoResponse : Photo, IQueryOrderable<PhotoResponse>
{ {
#region PROPERTIES #region PROPERTIES
[JsonIgnore]
public static IDictionary<string, Func<PhotoResponse, IComparable>> OrderableProperties { get; } = new Dictionary<string, Func<PhotoResponse, IComparable>>
{
{ "id", x => x.Id },
{ "media_id", x => x.MediaId },
{ "mime_type", x => x.MimeType },
{ "is_background", x => x.Background is not null },
{ "is_universal_background", x => x.Background is not null && x.Background.IsUniversalBackground }
};
[JsonPropertyName("id")] [JsonPropertyName("id")]
public Guid Id { get; set; } public Guid Id { get; set; }
[JsonPropertyName("media_id")]
public required long MediaId { get; set; }
[JsonPropertyName("upload_date")] [JsonPropertyName("upload_date")]
public DateTime UploadDate { get; set; } public DateTime UploadDate { get; set; }
@@ -21,10 +36,10 @@ public class MediaPhotoResponse : MediaPhoto
#region CONSTRUCTORS #region CONSTRUCTORS
[JsonConstructor] [JsonConstructor]
public MediaPhotoResponse() {} public PhotoResponse() {}
[SetsRequiredMembers] [SetsRequiredMembers]
public MediaPhotoResponse(MediaPhotoImage mediaPhotoImage) public PhotoResponse(MediaPhotoImage mediaPhotoImage)
{ {
Id = mediaPhotoImage.Id; Id = mediaPhotoImage.Id;
MediaId = mediaPhotoImage.MediaId; MediaId = mediaPhotoImage.MediaId;
@@ -34,7 +49,7 @@ public class MediaPhotoResponse : MediaPhoto
if (mediaPhotoImage.MediaPhotoImageBackground is not null) if (mediaPhotoImage.MediaPhotoImageBackground is not null)
{ {
Background = new MediaPhotoBackground Background = new PhotoBackgroundData
{ {
IsUniversalBackground = mediaPhotoImage.MediaPhotoImageBackground.IsUniversalBackground, IsUniversalBackground = mediaPhotoImage.MediaPhotoImageBackground.IsUniversalBackground,
FirstGradientColor = mediaPhotoImage.MediaPhotoImageBackground.FirstGradientColor, FirstGradientColor = mediaPhotoImage.MediaPhotoImageBackground.FirstGradientColor,

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model;
public abstract class Picture
{
#region PROPERTIES
[JsonPropertyName("image")]
public required byte[] Image { get; set; }
[JsonPropertyName("mime_type")]
public required string MimeType { get; set; }
#endregion
#region PUBLIC METHODS
public override string ToString() => $"data:{MimeType};base64,{Convert.ToBase64String(Image)}";
#endregion
}

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

@@ -0,0 +1,37 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using WatchIt.Database.Model.Rating;
namespace WatchIt.Common.Model.Rating;
public class RatingResponse
{
#region PROPERTIES
[JsonPropertyName("average")]
public required decimal Average { get; set; }
[JsonPropertyName("count")]
public required long Count { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public RatingResponse() {}
[SetsRequiredMembers]
public RatingResponse(IEnumerable<RatingMedia> ratingMedia) : this(ratingMedia.Any() ? (decimal)ratingMedia.Average(x => x.Rating) : 0, ratingMedia.Count()) {}
[SetsRequiredMembers]
public RatingResponse(decimal ratingAverage, long ratingCount)
{
Average = ratingAverage;
Count = ratingCount;
}
#endregion
}

View File

@@ -37,6 +37,24 @@ public class SeriesQueryParameters : QueryParameters<SeriesResponse>
[FromQuery(Name = "has_ended")] [FromQuery(Name = "has_ended")]
public bool? HasEnded { get; set; } public bool? HasEnded { get; set; }
[FromQuery(Name = "rating_average")]
public decimal? RatingAverage { get; set; }
[FromQuery(Name = "rating_average_from")]
public decimal? RatingAverageFrom { get; set; }
[FromQuery(Name = "rating_average_to")]
public decimal? RatingAverageTo { get; set; }
[FromQuery(Name = "rating_count")]
public long? RatingCount { get; set; }
[FromQuery(Name = "rating_count_from")]
public long? RatingCountFrom { get; set; }
[FromQuery(Name = "rating_count_to")]
public long? RatingCountTo { get; set; }
#endregion #endregion
@@ -45,17 +63,21 @@ public class SeriesQueryParameters : QueryParameters<SeriesResponse>
public override bool IsMeetingConditions(SeriesResponse item) => public override bool IsMeetingConditions(SeriesResponse item) =>
( (
TestString(item.Title, Title) TestStringWithRegex(item.Title, Title)
&& &&
TestString(item.OriginalTitle, OriginalTitle) TestStringWithRegex(item.OriginalTitle, OriginalTitle)
&& &&
TestString(item.Description, Description) TestStringWithRegex(item.Description, Description)
&& &&
TestComparable(item.ReleaseDate, ReleaseDate, ReleaseDateFrom, ReleaseDateTo) TestComparable(item.ReleaseDate, ReleaseDate, ReleaseDateFrom, ReleaseDateTo)
&& &&
TestComparable(item.Length, Length, LengthFrom, LengthTo) TestComparable(item.Length, Length, LengthFrom, LengthTo)
&& &&
TestBoolean(item.HasEnded, HasEnded) Test(item.HasEnded, HasEnded)
&&
TestComparable(item.Rating.Average, RatingAverage, RatingAverageFrom, RatingAverageTo)
&&
TestComparable(item.Rating.Count, RatingCount, RatingCountFrom, RatingCountTo)
); );
#endregion #endregion

View File

@@ -1,15 +1,35 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using WatchIt.Common.Model.Rating;
using WatchIt.Common.Query;
using WatchIt.Database.Model.Media; using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Series; namespace WatchIt.Common.Model.Series;
public class SeriesResponse : Series public class SeriesResponse : Series, IQueryOrderable<SeriesResponse>
{ {
#region PROPERTIES #region PROPERTIES
[JsonIgnore]
public static IDictionary<string, Func<SeriesResponse, IComparable>> OrderableProperties { get; } = new Dictionary<string, Func<SeriesResponse, IComparable>>
{
{ "id", x => x.Id },
{ "title", x => x.Title },
{ "original_title", x => x.OriginalTitle },
{ "description", x => x.Description },
{ "release_date", x => x.ReleaseDate },
{ "length", x => x.Length },
{ "has_ended", x => x.HasEnded },
{ "rating.average", x => x.Rating.Average },
{ "rating.count", x => x.Rating.Count }
};
[JsonPropertyName("id")] [JsonPropertyName("id")]
public long Id { get; set; } public long Id { get; set; }
[JsonPropertyName("rating")]
public RatingResponse Rating { get; set; }
#endregion #endregion
@@ -30,6 +50,7 @@ public class SeriesResponse : Series
ReleaseDate = mediaSeries.Media.ReleaseDate; ReleaseDate = mediaSeries.Media.ReleaseDate;
Length = mediaSeries.Media.Length; Length = mediaSeries.Media.Length;
HasEnded = mediaSeries.HasEnded; HasEnded = mediaSeries.HasEnded;
Rating = new RatingResponse(mediaSeries.Media.RatingMedia);
} }
#endregion #endregion

View File

@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Query;
public interface IQueryOrderable<T>
{
[JsonIgnore]
public static abstract IDictionary<string, Func<T, IComparable>> OrderableProperties { get; }
}

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Globalization;
using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -38,7 +39,12 @@ public abstract class QueryParameters
FromQueryAttribute? attribute = property.GetCustomAttributes<FromQueryAttribute>(true).FirstOrDefault(); FromQueryAttribute? attribute = property.GetCustomAttributes<FromQueryAttribute>(true).FirstOrDefault();
if (value is not null && attribute is not null) if (value is not null && attribute is not null)
{ {
string query = $"{attribute.Name}={value}"; string valueString = (value switch
{
decimal d => d.ToString(CultureInfo.InvariantCulture),
_ => value.ToString()
})!;
string query = $"{attribute.Name}={valueString}";
queries.Add(query); queries.Add(query);
} }
} }
@@ -52,21 +58,25 @@ public abstract class QueryParameters
#region PRIVATE METHODS #region PRIVATE METHODS
protected static bool TestBoolean(bool property, bool? query) => protected static bool Test<T>(T? property, T? query) =>
( (
query is null query is null
|| ||
property == query (
property is not null
&&
property.Equals(query)
)
); );
protected static bool TestString(string? property, string? regexQuery) => protected static bool TestStringWithRegex(string? property, string? regexQuery) =>
( (
string.IsNullOrEmpty(regexQuery) string.IsNullOrEmpty(regexQuery)
|| ||
( (
!string.IsNullOrEmpty(property) !string.IsNullOrEmpty(property)
&& &&
new Regex(regexQuery).IsMatch(property) Regex.IsMatch(property, regexQuery, RegexOptions.IgnoreCase)
) )
); );
@@ -88,7 +98,7 @@ public abstract class QueryParameters
( (
property is not null property is not null
&& &&
property.CompareTo(from) > 0 property.CompareTo(from) >= 0
) )
) )
&& &&
@@ -108,7 +118,7 @@ public abstract class QueryParameters
public abstract class QueryParameters<T> : QueryParameters where T : class public abstract class QueryParameters<T> : QueryParameters where T : IQueryOrderable<T>
{ {
#region PUBLIC METHODS #region PUBLIC METHODS
@@ -120,22 +130,9 @@ public abstract class QueryParameters<T> : QueryParameters where T : class
if (OrderBy is not null) if (OrderBy is not null)
{ {
PropertyInfo[] properties = typeof(T).GetProperties(); if (T.OrderableProperties.TryGetValue(OrderBy, out Func<T, IComparable>? orderFunc))
foreach (PropertyInfo property in properties)
{ {
JsonPropertyNameAttribute? attribute = property.GetCustomAttributes<JsonPropertyNameAttribute>(true).FirstOrDefault(); data = Order == "asc" ? data.OrderBy(orderFunc) : data.OrderByDescending(orderFunc);
if (attribute is not null && attribute.Name == OrderBy)
{
if (Order == "asc")
{
data = data.OrderBy(property.GetValue);
}
else
{
data = data.OrderByDescending(property.GetValue);
}
break;
}
} }
} }
if (After is not null) if (After is not null)

View File

@@ -4,33 +4,54 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; 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.Rating;
using WatchIt.WebAPI.Services.Controllers.Media; using WatchIt.WebAPI.Services.Controllers.Media;
namespace WatchIt.WebAPI.Controllers; namespace WatchIt.WebAPI.Controllers;
[ApiController] [ApiController]
[Route("media")] [Route("media")]
public class MediaController(IMediaControllerService mediaControllerService) public class MediaController : ControllerBase
{ {
#region MAIN #region FIELDS
[HttpGet("{id}")] private readonly IMediaControllerService _mediaControllerService;
[AllowAnonymous]
[ProducesResponseType(typeof(MediaResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMedia([FromRoute] long id) => await mediaControllerService.GetMedia(id);
#endregion #endregion
#region GENRES #region CONSTRUCTORS
public MediaController(IMediaControllerService mediaControllerService)
{
_mediaControllerService = mediaControllerService;
}
#endregion
#region METHODS
#region Main
[HttpGet("{id}")]
[AllowAnonymous]
[ProducesResponseType(typeof(MediaResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMedia([FromRoute] long id) => await _mediaControllerService.GetMedia(id);
#endregion
#region Genres
[HttpGet("{id}/genres")] [HttpGet("{id}/genres")]
[AllowAnonymous] [AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<GenreResponse>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(IEnumerable<GenreResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaGenres([FromRoute]long id) => await mediaControllerService.GetMediaGenres(id); public async Task<ActionResult> GetMediaGenres([FromRoute]long id) => await _mediaControllerService.GetMediaGenres(id);
[HttpPost("{id}/genres/{genre_id}")] [HttpPost("{id}/genres/{genre_id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
@@ -38,7 +59,7 @@ public class MediaController(IMediaControllerService mediaControllerService)
[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)]
public async Task<ActionResult> PostMediaGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await mediaControllerService.PostMediaGenre(id, genreId); public async Task<ActionResult> PostMediaGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await _mediaControllerService.PostMediaGenre(id, genreId);
[HttpDelete("{id}/genres/{genre_id}")] [HttpDelete("{id}/genres/{genre_id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
@@ -46,25 +67,23 @@ public class MediaController(IMediaControllerService mediaControllerService)
[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)]
public async Task<ActionResult> DeleteMediaGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await mediaControllerService.DeleteMediaGenre(id, genreId); public async Task<ActionResult> DeleteMediaGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await _mediaControllerService.DeleteMediaGenre(id, genreId);
#endregion #endregion
#region Rating
#region RATING
[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);
[HttpGet("{id}/rating/{user_id}")] [HttpGet("{id}/rating/{user_id}")]
[AllowAnonymous] [AllowAnonymous]
[ProducesResponseType(typeof(short), StatusCodes.Status200OK)] [ProducesResponseType(typeof(short), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaRatingByUser([FromRoute] long id, [FromRoute(Name = "user_id")]long userId) => await mediaControllerService.GetMediaRatingByUser(id, userId); public async Task<ActionResult> GetMediaRatingByUser([FromRoute] long id, [FromRoute(Name = "user_id")]long userId) => await _mediaControllerService.GetMediaRatingByUser(id, userId);
[HttpPut("{id}/rating")] [HttpPut("{id}/rating")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
@@ -72,38 +91,34 @@ public class MediaController(IMediaControllerService mediaControllerService)
[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)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> DeleteMediaRating([FromRoute] long id) => await mediaControllerService.DeleteMediaRating(id); public async Task<ActionResult> DeleteMediaRating([FromRoute] long id) => await _mediaControllerService.DeleteMediaRating(id);
#endregion #endregion
#region View count
#region VIEW COUNT
[HttpPost("{id}/view")] [HttpPost("{id}/view")]
[AllowAnonymous] [AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PostMediaView([FromRoute] long id) => await mediaControllerService.PostMediaView(id); public async Task<ActionResult> PostMediaView([FromRoute] long id) => await _mediaControllerService.PostMediaView(id);
#endregion #endregion
#region Poster
#region POSTER
[HttpGet("{id}/poster")] [HttpGet("{id}/poster")]
[AllowAnonymous] [AllowAnonymous]
[ProducesResponseType(typeof(MediaPosterResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(MediaPosterResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaPoster([FromRoute] long id) => await mediaControllerService.GetMediaPoster(id); public async Task<ActionResult> GetMediaPoster([FromRoute] long id) => await _mediaControllerService.GetMediaPoster(id);
[HttpPut("{id}/poster")] [HttpPut("{id}/poster")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
@@ -111,68 +126,41 @@ public class MediaController(IMediaControllerService mediaControllerService)
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> PutMediaPoster([FromRoute]long id, [FromBody]MediaPosterRequest body) => await mediaControllerService.PutMediaPoster(id, body); public async Task<ActionResult> PutMediaPoster([FromRoute]long id, [FromBody]MediaPosterRequest body) => await _mediaControllerService.PutMediaPoster(id, body);
[HttpDelete("{id}/poster")] [HttpDelete("{id}/poster")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteMediaPoster([FromRoute]long id) => await mediaControllerService.DeleteMediaPoster(id); public async Task<ActionResult> DeleteMediaPoster([FromRoute]long id) => await _mediaControllerService.DeleteMediaPoster(id);
#endregion #endregion
#region Photos
[HttpGet("{id}/photos")]
#region PHOTOS [AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<PhotoResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaPhotos([FromRoute]long id, PhotoQueryParameters query) => await _mediaControllerService.GetMediaPhotos(id, query);
[HttpGet("{id}/photos/random_background")] [HttpGet("{id}/photos/random_background")]
[AllowAnonymous] [AllowAnonymous]
[ProducesResponseType(typeof(MediaPhotoResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PhotoResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaPhotoRandomBackground([FromRoute]long id) => await mediaControllerService.GetMediaRandomBackgroundPhoto(id); public async Task<ActionResult> GetMediaPhotoRandomBackground([FromRoute]long id) => await _mediaControllerService.GetMediaPhotoRandomBackground(id);
[HttpGet("photos/{photo_id}")] [HttpPost("{id}/photos")]
[AllowAnonymous]
[ProducesResponseType(typeof(MediaPhotoResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetPhoto([FromRoute(Name = "photo_id")] Guid photoId) => await mediaControllerService.GetPhoto(photoId);
[HttpGet("photos")]
[AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<MediaPhotoResponse>), StatusCodes.Status200OK)]
public async Task<ActionResult> GetPhotos(MediaPhotoQueryParameters query) => await mediaControllerService.GetPhotos(query);
[HttpGet("photos/random_background")]
[AllowAnonymous]
[ProducesResponseType(typeof(MediaPhotoResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetPhotoRandomBackground() => await mediaControllerService.GetRandomBackgroundPhoto();
[HttpPost("photos")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(MediaPhotoResponse), StatusCodes.Status201Created)] [ProducesResponseType(typeof(PhotoResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> PostPhoto([FromBody]MediaPhotoRequest body) => await mediaControllerService.PostPhoto(body);
[HttpPut("photos/{photo_id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [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)]
public async Task<ActionResult> PutPhoto([FromRoute(Name = "photo_id")]Guid photoId, [FromBody]MediaPhotoRequest body) => await mediaControllerService.PutPhoto(photoId, body); public async Task<ActionResult> PostPhoto([FromRoute]long id, [FromBody]MediaPhotoRequest body) => await _mediaControllerService.PostMediaPhoto(id, body);
[HttpDelete("photos/{photo_id}")] #endregion
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeletePhoto([FromRoute(Name = "photo_id")]Guid photoId) => await mediaControllerService.DeletePhoto(photoId);
#endregion #endregion
} }

View File

@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Model.Photos;
using WatchIt.WebAPI.Services.Controllers.Photos;
namespace WatchIt.WebAPI.Controllers;
[ApiController]
[Route("photos")]
public class PhotosController : ControllerBase
{
#region FIELDS
private readonly IPhotosControllerService _photosControllerService;
#endregion
#region CONSTRUCTORS
public PhotosController(IPhotosControllerService photosControllerService)
{
_photosControllerService = photosControllerService;
}
#endregion
#region METHODS
#region Main
[HttpGet("random_background")]
[AllowAnonymous]
[ProducesResponseType(typeof(PhotoResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetPhotoRandomBackground() => await _photosControllerService.GetPhotoRandomBackground();
[HttpDelete("{id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeletePhoto([FromRoute] Guid id) => await _photosControllerService.DeletePhoto(id);
#endregion
#region Background data
[HttpPut("{id}/background_data")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PutPhotoBackgroundData([FromRoute]Guid id, [FromBody] PhotoBackgroundDataRequest body) => await _photosControllerService.PutPhotoBackgroundData(id, body);
[HttpDelete("{id}/background_data")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeletePhotoBackgroundData([FromRoute]Guid id) => await _photosControllerService.DeletePhotoBackgroundData(id);
#endregion
#endregion
}

View File

@@ -17,6 +17,7 @@
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Genres\WatchIt.WebAPI.Services.Controllers.Genres.csproj" /> <ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Genres\WatchIt.WebAPI.Services.Controllers.Genres.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Media\WatchIt.WebAPI.Services.Controllers.Media.csproj" /> <ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Media\WatchIt.WebAPI.Services.Controllers.Media.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Movies\WatchIt.WebAPI.Services.Controllers.Movies.csproj" /> <ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Movies\WatchIt.WebAPI.Services.Controllers.Movies.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Photos\WatchIt.WebAPI.Services.Controllers.Photos.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Series\WatchIt.WebAPI.Services.Controllers.Series.csproj" /> <ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Series\WatchIt.WebAPI.Services.Controllers.Series.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,4 +1,6 @@
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
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;
@@ -13,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);
@@ -21,12 +23,8 @@ public interface IMediaControllerService
Task<RequestResult> GetMediaPoster(long mediaId); Task<RequestResult> GetMediaPoster(long mediaId);
Task<RequestResult> PutMediaPoster(long mediaId, MediaPosterRequest data); Task<RequestResult> PutMediaPoster(long mediaId, MediaPosterRequest data);
Task<RequestResult> DeleteMediaPoster(long mediaId); Task<RequestResult> DeleteMediaPoster(long mediaId);
Task<RequestResult> GetPhoto(Guid id); Task<RequestResult> GetMediaPhotos(long mediaId, PhotoQueryParameters queryParameters);
Task<RequestResult> GetPhotos(MediaPhotoQueryParameters query); Task<RequestResult> GetMediaPhotoRandomBackground(long mediaId);
Task<RequestResult> GetRandomBackgroundPhoto(); Task<RequestResult> PostMediaPhoto(long mediaId, MediaPhotoRequest data);
Task<RequestResult> GetMediaRandomBackgroundPhoto(long id);
Task<RequestResult> PostPhoto(MediaPhotoRequest data);
Task<RequestResult> PutPhoto(Guid photoId, MediaPhotoRequest data);
Task<RequestResult> DeletePhoto(Guid photoId);
} }

View File

@@ -2,6 +2,8 @@
using SimpleToolkit.Extensions; 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.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;
@@ -105,10 +107,8 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
{ {
return RequestResult.NotFound(); return RequestResult.NotFound();
} }
double ratingAverage = item.RatingMedia.Any() ? item.RatingMedia.Average(x => x.Rating) : 0; RatingResponse ratingResponse = new RatingResponse(item.RatingMedia);
long ratingCount = item.RatingMedia.Count();
MediaRatingResponse ratingResponse = new MediaRatingResponse(ratingAverage, ratingCount);
return RequestResult.Ok(ratingResponse); return RequestResult.Ok(ratingResponse);
} }
@@ -130,7 +130,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)
@@ -294,59 +294,48 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
#endregion #endregion
#region Photos #region Photos
public async Task<RequestResult> GetPhoto(Guid id) public async Task<RequestResult> GetMediaPhotos(long mediaId, PhotoQueryParameters queryParameters)
{ {
MediaPhotoImage? item = await database.MediaPhotoImages.FirstOrDefaultAsync(x => x.Id == id); Database.Model.Media.Media? media = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (item is null) if (media is null)
{ {
return RequestResult.NotFound(); return RequestResult.NotFound();
} }
MediaPhotoResponse data = new MediaPhotoResponse(item); IEnumerable<MediaPhotoImage> imagesRaw = await database.MediaPhotoImages.Where(x => x.MediaId == mediaId).ToListAsync();
return RequestResult.Ok(data); IEnumerable<PhotoResponse> images = imagesRaw.Select(x => new PhotoResponse(x));
images = queryParameters.PrepareData(images);
return RequestResult.Ok(images);
} }
public async Task<RequestResult> GetPhotos(MediaPhotoQueryParameters query) public Task<RequestResult> GetMediaPhotoRandomBackground(long mediaId)
{ {
IEnumerable<MediaPhotoResponse> data = await database.MediaPhotoImages.Select(x => new MediaPhotoResponse(x)).ToListAsync(); MediaPhotoImage? image = database.MediaPhotoImages.Where(x => x.MediaId == mediaId && x.MediaPhotoImageBackground != null).Random();
data = query.PrepareData(data);
return RequestResult.Ok(data);
}
public Task<RequestResult> GetRandomBackgroundPhoto()
{
MediaPhotoImage? image = database.MediaPhotoImages.Where(x => x.MediaPhotoImageBackground != null && x.MediaPhotoImageBackground.IsUniversalBackground).Random();
if (image is null) if (image is null)
{ {
return Task.FromResult<RequestResult>(RequestResult.NotFound()); return Task.FromResult<RequestResult>(RequestResult.NotFound());
} }
MediaPhotoResponse data = new MediaPhotoResponse(image); PhotoResponse data = new PhotoResponse(image);
return Task.FromResult<RequestResult>(RequestResult.Ok(data));
}
public Task<RequestResult> GetMediaRandomBackgroundPhoto(long id)
{
MediaPhotoImage? image = database.MediaPhotoImages.Where(x => x.MediaId == id && x.MediaPhotoImageBackground != null).Random();
if (image is null)
{
return Task.FromResult<RequestResult>(RequestResult.NotFound());
}
MediaPhotoResponse data = new MediaPhotoResponse(image);
return Task.FromResult<RequestResult>(RequestResult.Ok(data)); return Task.FromResult<RequestResult>(RequestResult.Ok(data));
} }
public async Task<RequestResult> PostPhoto(MediaPhotoRequest data) public async Task<RequestResult> PostMediaPhoto(long mediaId, MediaPhotoRequest data)
{ {
UserValidator validator = userService.GetValidator().MustBeAdmin(); UserValidator validator = userService.GetValidator().MustBeAdmin();
if (!validator.IsValid) if (!validator.IsValid)
{ {
return RequestResult.Forbidden(); return RequestResult.Forbidden();
} }
Database.Model.Media.Media? media = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (media is null)
{
return RequestResult.NotFound();
}
MediaPhotoImage item = data.CreateMediaPhotoImage(); MediaPhotoImage item = data.CreateMediaPhotoImage(mediaId);
await database.MediaPhotoImages.AddAsync(item); await database.MediaPhotoImages.AddAsync(item);
await database.SaveChangesAsync(); await database.SaveChangesAsync();
@@ -357,69 +346,7 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
await database.SaveChangesAsync(); await database.SaveChangesAsync();
} }
return RequestResult.Created($"photos/{item.Id}", new MediaPhotoResponse(item)); return RequestResult.Created($"photos/{item.Id}", new PhotoResponse(item));
}
public async Task<RequestResult> PutPhoto(Guid photoId, MediaPhotoRequest data)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaPhotoImage? item = await database.MediaPhotoImages.FirstOrDefaultAsync(x => x.Id == photoId);
if (item is null)
{
return RequestResult.NotFound();
}
data.UpdateMediaPhotoImage(item);
if (item.MediaPhotoImageBackground is null && data.Background is not null)
{
MediaPhotoImageBackground background = data.CreateMediaPhotoImageBackground(item.Id)!;
await database.MediaPhotoImageBackgrounds.AddAsync(background);
}
else if (item.MediaPhotoImageBackground is not null && data.Background is null)
{
database.MediaPhotoImageBackgrounds.Attach(item.MediaPhotoImageBackground);
database.MediaPhotoImageBackgrounds.Remove(item.MediaPhotoImageBackground);
}
else if (item.MediaPhotoImageBackground is not null && data.Background is not null)
{
data.UpdateMediaPhotoImageBackground(item.MediaPhotoImageBackground);
}
await database.SaveChangesAsync();
return RequestResult.Ok();
}
public async Task<RequestResult> DeletePhoto(Guid photoId)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaPhotoImage? item = await database.MediaPhotoImages.FirstOrDefaultAsync(x => x.Id == photoId);
if (item is null)
{
return RequestResult.NotFound();
}
if (item.MediaPhotoImageBackground is not null)
{
database.MediaPhotoImageBackgrounds.Attach(item.MediaPhotoImageBackground);
database.MediaPhotoImageBackgrounds.Remove(item.MediaPhotoImageBackground);
await database.SaveChangesAsync();
}
database.MediaPhotoImages.Attach(item);
database.MediaPhotoImages.Remove(item);
await database.SaveChangesAsync();
return RequestResult.Ok();
} }
#endregion #endregion

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

@@ -0,0 +1,13 @@
using WatchIt.Common.Model.Photos;
using WatchIt.WebAPI.Services.Controllers.Common;
namespace WatchIt.WebAPI.Services.Controllers.Photos;
public interface IPhotosControllerService
{
Task<RequestResult> GetPhotoRandomBackground();
Task<RequestResult> DeletePhoto(Guid photoId);
Task<RequestResult> PutPhotoBackgroundData(Guid id, PhotoBackgroundDataRequest data);
Task<RequestResult> DeletePhotoBackgroundData(Guid id);
}

View File

@@ -0,0 +1,140 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SimpleToolkit.Extensions;
using WatchIt.Common.Model.Photos;
using WatchIt.Database;
using WatchIt.Database.Model.Media;
using WatchIt.WebAPI.Services.Controllers.Common;
using WatchIt.WebAPI.Services.Utility.User;
namespace WatchIt.WebAPI.Services.Controllers.Photos;
public class PhotosControllerService : IPhotosControllerService
{
#region FIELDS
private readonly DatabaseContext _database;
private readonly IUserService _userService;
#endregion
#region CONTRUCTORS
public PhotosControllerService(DatabaseContext database, IUserService userService)
{
_database = database;
_userService = userService;
}
#endregion
#region PUBLIC METHODS
#region Main
public Task<RequestResult> GetPhotoRandomBackground()
{
MediaPhotoImage? image = _database.MediaPhotoImages.Where(x => x.MediaPhotoImageBackground != null && x.MediaPhotoImageBackground.IsUniversalBackground).Random();
if (image is null)
{
return Task.FromResult<RequestResult>(RequestResult.NotFound());
}
PhotoResponse data = new PhotoResponse(image);
return Task.FromResult<RequestResult>(RequestResult.Ok(data));
}
public async Task<RequestResult> DeletePhoto(Guid photoId)
{
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaPhotoImage? item = await _database.MediaPhotoImages.FirstOrDefaultAsync(x => x.Id == photoId);
if (item is null)
{
return RequestResult.NotFound();
}
if (item.MediaPhotoImageBackground is not null)
{
_database.MediaPhotoImageBackgrounds.Attach(item.MediaPhotoImageBackground);
_database.MediaPhotoImageBackgrounds.Remove(item.MediaPhotoImageBackground);
await _database.SaveChangesAsync();
}
_database.MediaPhotoImages.Attach(item);
_database.MediaPhotoImages.Remove(item);
await _database.SaveChangesAsync();
return RequestResult.Ok();
}
#endregion
#region Background data
public async Task<RequestResult> PutPhotoBackgroundData(Guid id, PhotoBackgroundDataRequest data)
{
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaPhotoImage? image = await _database.MediaPhotoImages.FirstOrDefaultAsync(x => x.Id == id);
if (image is null)
{
return RequestResult.NotFound();
}
MediaPhotoImageBackground? imageBackground = image.MediaPhotoImageBackground;
if (imageBackground is null)
{
imageBackground = data.CreateMediaPhotoImageBackground(id);
await _database.MediaPhotoImageBackgrounds.AddAsync(imageBackground);
}
else
{
data.UpdateMediaPhotoImageBackground(imageBackground);
}
await _database.SaveChangesAsync();
return RequestResult.Ok();
}
public async Task<RequestResult> DeletePhotoBackgroundData(Guid id)
{
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaPhotoImage? image = await _database.MediaPhotoImages.FirstOrDefaultAsync(x => x.Id == id);
if (image is null)
{
return RequestResult.NotFound();
}
MediaPhotoImageBackground? imageBackground = image.MediaPhotoImageBackground;
if (imageBackground is not null)
{
_database.MediaPhotoImageBackgrounds.Attach(imageBackground);
_database.MediaPhotoImageBackgrounds.Remove(imageBackground);
await _database.SaveChangesAsync();
}
return RequestResult.Ok();
}
#endregion
#endregion
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Model\WatchIt.Common.Model.csproj" />
<ProjectReference Include="..\..\..\..\WatchIt.Database\WatchIt.Database\WatchIt.Database.csproj" />
<ProjectReference Include="..\..\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.User\WatchIt.WebAPI.Services.Utility.User.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services.Controllers.Common\WatchIt.WebAPI.Services.Controllers.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SimpleToolkit.Extensions" Version="1.7.7" />
</ItemGroup>
</Project>

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

@@ -2,20 +2,19 @@
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Database; using WatchIt.Database;
using WatchIt.WebAPI.Validators.Photos;
namespace WatchIt.WebAPI.Validators.Media; namespace WatchIt.WebAPI.Validators.Media;
public class MediaPhotoRequestValidator : AbstractValidator<MediaPhotoRequest> public class MediaPhotoRequestValidator : AbstractValidator<MediaPhotoRequest>
{ {
public MediaPhotoRequestValidator(DatabaseContext database) public MediaPhotoRequestValidator()
{ {
RuleFor(x => x.MediaId).MustBeIn(database.Media, x => x.Id).WithMessage("Media does not exists");
RuleFor(x => x.Image).NotEmpty(); RuleFor(x => x.Image).NotEmpty();
RuleFor(x => x.MimeType).Matches(@"\w+/.+").WithMessage("Incorrect mimetype"); RuleFor(x => x.MimeType).Matches(@"\w+/.+").WithMessage("Incorrect mimetype");
When(x => x.Background is not null, () => When(x => x.Background is not null, () =>
{ {
RuleFor(x => x.Background!.FirstGradientColor).Must(x => x.Length == 3).WithMessage("First gradient color has to be 3 byte long"); RuleFor(x => x.Background!).SetValidator(new PhotoBackgroundDataValidator());
RuleFor(x => x.Background!.SecondGradientColor).Must(x => x.Length == 3).WithMessage("Second gradient color has to be 3 byte long");
}); });
} }
} }

View File

@@ -0,0 +1,12 @@
using FluentValidation;
using WatchIt.Common.Model.Photos;
namespace WatchIt.WebAPI.Validators.Photos;
public class PhotoBackgroundDataRequestValidator : AbstractValidator<PhotoBackgroundDataRequest>
{
public PhotoBackgroundDataRequestValidator()
{
RuleFor(x => x).SetValidator(new PhotoBackgroundDataValidator());
}
}

View File

@@ -0,0 +1,13 @@
using FluentValidation;
using WatchIt.Common.Model.Photos;
namespace WatchIt.WebAPI.Validators.Photos;
public class PhotoBackgroundDataValidator : AbstractValidator<PhotoBackgroundData>
{
public PhotoBackgroundDataValidator()
{
RuleFor(x => x.FirstGradientColor).Must(x => x.Length == 3).WithMessage("First gradient color has to be 3 byte long");
RuleFor(x => x.SecondGradientColor).Must(x => x.Length == 3).WithMessage("Second gradient color has to be 3 byte long");
}
}

View File

@@ -13,6 +13,7 @@ using WatchIt.WebAPI.Services.Controllers.Accounts;
using WatchIt.WebAPI.Services.Controllers.Genres; using WatchIt.WebAPI.Services.Controllers.Genres;
using WatchIt.WebAPI.Services.Controllers.Media; using WatchIt.WebAPI.Services.Controllers.Media;
using WatchIt.WebAPI.Services.Controllers.Movies; using WatchIt.WebAPI.Services.Controllers.Movies;
using WatchIt.WebAPI.Services.Controllers.Photos;
using WatchIt.WebAPI.Services.Controllers.Series; using WatchIt.WebAPI.Services.Controllers.Series;
using WatchIt.WebAPI.Services.Utility.Configuration; using WatchIt.WebAPI.Services.Utility.Configuration;
using WatchIt.WebAPI.Services.Utility.Tokens; using WatchIt.WebAPI.Services.Utility.Tokens;
@@ -156,6 +157,7 @@ public static class Program
builder.Services.AddTransient<IMoviesControllerService, MoviesControllerService>(); builder.Services.AddTransient<IMoviesControllerService, MoviesControllerService>();
builder.Services.AddTransient<IMediaControllerService, MediaControllerService>(); builder.Services.AddTransient<IMediaControllerService, MediaControllerService>();
builder.Services.AddTransient<ISeriesControllerService, SeriesControllerService>(); builder.Services.AddTransient<ISeriesControllerService, SeriesControllerService>();
builder.Services.AddTransient<IPhotosControllerService, PhotosControllerService>();
return builder; return builder;
} }

View File

@@ -8,4 +8,5 @@ public class Endpoints
public Media Media { get; set; } public Media Media { get; set; }
public Movies Movies { get; set; } public Movies Movies { get; set; }
public Series Series { get; set; } public Series Series { get; set; }
public Photos Photos { get; set; }
} }

View File

@@ -3,23 +3,19 @@
public class Media public class Media
{ {
public string Base { get; set; } public string Base { get; set; }
public string Get { get; set; } public string GetMedia { get; set; }
public string GetGenres { get; set; } public string GetMediaGenres { get; set; }
public string PostGenre { get; set; } public string PostMediaGenre { get; set; }
public string DeleteGenre { get; set; } public string DeleteMediaGenre { get; set; }
public string GetMediaRating { get; set; } public string GetMediaRating { get; set; }
public string GetMediaRatingByUser { get; set; } public string GetMediaRatingByUser { get; set; }
public string PutMediaRating { get; set; } public string PutMediaRating { get; set; }
public string DeleteMediaRating { get; set; } public string DeleteMediaRating { get; set; }
public string PostMediaView { get; set; } public string PostMediaView { get; set; }
public string GetPhotoMediaRandomBackground { get; set; } public string GetMediaPoster { get; set; }
public string GetPoster { get; set; } public string PutMediaPoster { get; set; }
public string PutPoster { get; set; } public string DeleteMediaPoster { get; set; }
public string DeletePoster { get; set; } public string GetMediaPhotos { get; set; }
public string GetPhoto { get; set; } public string GetMediaPhotoRandomBackground { get; set; }
public string GetPhotos { get; set; } public string PostMediaPhoto { get; set; }
public string GetPhotoRandomBackground { get; set; }
public string PostPhoto { get; set; }
public string PutPhoto { get; set; }
public string DeletePhoto { get; set; }
} }

View File

@@ -0,0 +1,10 @@
namespace WatchIt.Website.Services.Utility.Configuration.Model;
public class Photos
{
public string Base { get; set; }
public string GetPhotoRandomBackground { get; set; }
public string DeletePhoto { get; set; }
public string PutPhotoBackgroundData { get; set; }
public string DeletePhotoBackgroundData { get; set; }
}

View File

@@ -1,5 +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.Rating;
namespace WatchIt.Website.Services.WebAPI.Media; namespace WatchIt.Website.Services.WebAPI.Media;
@@ -9,17 +11,20 @@ public interface IMediaWebAPIService
Task GetMediaGenres(long mediaId, Action<IEnumerable<GenreResponse>>? successAction = null, Action? notFoundAction = null); Task GetMediaGenres(long mediaId, Action<IEnumerable<GenreResponse>>? successAction = null, Action? notFoundAction = null);
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 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);
Task GetPhotoMediaRandomBackground(long mediaId, Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null); Task GetMediaPoster(long mediaId, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null);
Task GetPhotoRandomBackground(Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null); Task PutMediaPoster(long mediaId, MediaPosterRequest data, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task GetPoster(long mediaId, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null); Task DeleteMediaPoster(long mediaId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task PutPoster(long mediaId, MediaPosterRequest data, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task DeletePoster(long mediaId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null); Task GetMediaPhotos(long mediaId, PhotoQueryParameters? query = null, Action<IEnumerable<PhotoResponse>>? successAction = null, Action? notFoundAction = null);
Task GetMediaPhotoRandomBackground(long mediaId, Action<PhotoResponse>? successAction = null, Action? notFoundAction = null);
Task PostMediaPhoto(long mediaId, MediaPhotoRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null);
} }

View File

@@ -1,5 +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.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;
@@ -7,19 +9,38 @@ using WatchIt.Website.Services.WebAPI.Common;
namespace WatchIt.Website.Services.WebAPI.Media; namespace WatchIt.Website.Services.WebAPI.Media;
public class MediaWebAPIService(IHttpClientService httpClientService, IConfigurationService configurationService) : BaseWebAPIService(configurationService), IMediaWebAPIService public class MediaWebAPIService : BaseWebAPIService, IMediaWebAPIService
{ {
#region FIELDS
private readonly IHttpClientService _httpClientService;
#endregion
#region CONSTRUCTORS
public MediaWebAPIService(IHttpClientService httpClientService, IConfigurationService configurationService) : base(configurationService)
{
_httpClientService = httpClientService;
}
#endregion
#region PUBLIC METHODS #region PUBLIC METHODS
#region Main #region Main
public async Task GetMedia(long mediaId, Action<MediaResponse>? successAction = null, Action? notFoundAction = null) public async Task GetMedia(long mediaId, Action<MediaResponse>? successAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.Get, mediaId); string url = GetUrl(EndpointsConfiguration.Media.GetMedia, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url); HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction) .RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction(); .ExecuteAction();
@@ -31,11 +52,11 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
public async Task GetMediaGenres(long mediaId, Action<IEnumerable<GenreResponse>>? successAction = null, Action? notFoundAction = null) public async Task GetMediaGenres(long mediaId, Action<IEnumerable<GenreResponse>>? successAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.GetGenres, mediaId); string url = GetUrl(EndpointsConfiguration.Media.GetMediaGenres, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url); HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction) .RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction(); .ExecuteAction();
@@ -43,11 +64,25 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
public async Task PostMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null) public async Task PostMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.PostGenre, mediaId, genreId); string url = GetUrl(EndpointsConfiguration.Media.PostMediaGenre, mediaId, genreId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url); HttpRequest request = new HttpRequest(HttpMethodType.Post, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task DeleteMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.DeleteMediaGenre, mediaId, genreId);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction) .RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction) .RegisterActionFor403Forbidden(forbiddenAction)
@@ -59,13 +94,13 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
#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);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url); HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction) .RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction(); .ExecuteAction();
@@ -77,13 +112,13 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
HttpRequest request = new HttpRequest(HttpMethodType.Get, url); HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction) .RegisterActionFor404NotFound(notFoundAction)
.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);
@@ -92,7 +127,7 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
Body = body Body = body
}; };
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction) .RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction) .RegisterActionFor401Unauthorized(unauthorizedAction)
@@ -106,7 +141,7 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url); HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction) .RegisterActionFor401Unauthorized(unauthorizedAction)
.ExecuteAction(); .ExecuteAction();
@@ -122,7 +157,7 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
HttpRequest request = new HttpRequest(HttpMethodType.Post, url); HttpRequest request = new HttpRequest(HttpMethodType.Post, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction) .RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction(); .ExecuteAction();
@@ -130,55 +165,31 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
#endregion #endregion
#region Poster
public async Task GetMediaPoster(long mediaId, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null)
public async Task GetPhotoMediaRandomBackground(long mediaId, Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.GetPhotoMediaRandomBackground, mediaId); string url = GetUrl(EndpointsConfiguration.Media.GetMediaPoster, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url); HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task GetPhotoRandomBackground(Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetPhotoRandomBackground);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task GetPoster(long mediaId, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetPoster, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction) .RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor404NotFound(notFoundAction) .RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction(); .ExecuteAction();
} }
public async Task PutPoster(long mediaId, MediaPosterRequest data, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null) public async Task PutMediaPoster(long mediaId, MediaPosterRequest data, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.PutPoster, mediaId); string url = GetUrl(EndpointsConfiguration.Media.PutMediaPoster, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url) HttpRequest request = new HttpRequest(HttpMethodType.Put, url)
{ {
Body = data Body = data
}; };
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction) .RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction) .RegisterActionFor401Unauthorized(unauthorizedAction)
@@ -186,18 +197,66 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
.ExecuteAction(); .ExecuteAction();
} }
public async Task DeletePoster(long mediaId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null) public async Task DeleteMediaPoster(long mediaId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{ {
string url = GetUrl(EndpointsConfiguration.Media.DeletePoster, mediaId); string url = GetUrl(EndpointsConfiguration.Media.DeleteMediaPoster, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url); HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await httpClientService.SendRequestAsync(request); HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction) response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction) .RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction) .RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction(); .ExecuteAction();
} }
#endregion
#region Photos
public async Task GetMediaPhotos(long mediaId, PhotoQueryParameters? query = null, Action<IEnumerable<PhotoResponse>>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetMediaPhotos, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task GetMediaPhotoRandomBackground(long mediaId, Action<PhotoResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetMediaPhotoRandomBackground, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task PostMediaPhoto(long mediaId, MediaPhotoRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.PostMediaPhoto, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Post, url)
{
Body = data
};
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
#endregion
#endregion #endregion

View File

@@ -0,0 +1,11 @@
using WatchIt.Common.Model.Photos;
namespace WatchIt.Website.Services.WebAPI.Photos;
public interface IPhotosWebAPIService
{
Task GetPhotoRandomBackground(Action<PhotoResponse>? successAction = 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? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = 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

@@ -0,0 +1,106 @@
using WatchIt.Common.Model.Photos;
using WatchIt.Common.Services.HttpClient;
using WatchIt.Website.Services.Utility.Configuration;
using WatchIt.Website.Services.WebAPI.Common;
namespace WatchIt.Website.Services.WebAPI.Photos;
public class PhotosWebAPIService : BaseWebAPIService, IPhotosWebAPIService
{
#region FIELDS
private readonly IHttpClientService _httpClientService;
#endregion
#region CONSTRUCTORS
public PhotosWebAPIService(IHttpClientService httpClientService, IConfigurationService configurationService) : base(configurationService)
{
_httpClientService = httpClientService;
}
#endregion
#region PUBLIC METHODS
#region Main
public async Task GetPhotoRandomBackground(Action<PhotoResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Photos.GetPhotoRandomBackground);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
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);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
#endregion
#region Background data
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);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url)
{
Body = data
};
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
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);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
#endregion
#endregion
#region PRIVATE METHODS
protected override string GetServiceBase() => EndpointsConfiguration.Photos.Base;
#endregion
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Services\WatchIt.Common.Services.HttpClient\WatchIt.Common.Services.HttpClient.csproj" />
<ProjectReference Include="..\..\WatchIt.Website.Services.Utility\WatchIt.Website.Services.Utility.Configuration\WatchIt.Website.Services.Utility.Configuration.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services.WebAPI.Common\WatchIt.Website.Services.WebAPI.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,12 +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.1.0.16"/> <link rel="stylesheet" href="css/general.css?version=0.2.0.3"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.1.0.26"/> <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.12"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- 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">

View File

@@ -0,0 +1,91 @@
@typeparam TItem where TItem : WatchIt.Common.Query.IQueryOrderable<TItem>
@typeparam TQuery where TQuery : WatchIt.Common.Query.QueryParameters<TItem>
<CascadingValue Value="this">
<div class="vstack gap-3">
<div class="rounded-3 panel panel-regular p-2 px-3 z-3">
<div class="container-grid">
<div class="row gx-3">
<div class="col">
<h2 class="m-0">@(Title)</h2>
</div>
<div class="col-auto align-self-center">
<div class="input-group input-group-sm">
<span class="input-group-text">Order by</span>
<select class="form-select" @onchange="SortingOptionChanged">
@foreach (KeyValuePair<string, string> sortingOption in SortingOptions)
{
<option value="@(sortingOption.Key)">@(sortingOption.Value)</option>
}
</select>
<input type="checkbox" class="btn-check" id="sortingAscending" autocomplete="off" @onchange="SortingAscendingChanged">
<label class="btn btn-outline-secondary" for="sortingAscending">&#8595;&#xFE0E;</label>
</div>
</div>
<div class="col-auto align-self-center">
<div class="d-flex">
<Dropdown RightAligned>
<DropdownToggle Color="Color.Secondary" Size="Size.Small">
<i class="fa fa-filter"></i>
</DropdownToggle>
<DropdownMenu Class="p-2">
<DropdownHeader>Filters</DropdownHeader>
<DropdownDivider/>
@(ChildContent)
<DropdownDivider/>
<button class="btn btn-secondary btn-sm w-100" @onclick="FilterApplied">Apply</button>
</DropdownMenu>
</Dropdown>
</div>
</div>
</div>
</div>
</div>
@if (_loaded)
{
if (string.IsNullOrWhiteSpace(_error))
{
foreach (TItem item in _items)
{
<div role="button" class="rounded-3 panel panel-regular p-2" @onclick="@(() => NavigationManager.NavigateTo(string.Format(UrlIdTemplate, IdSource(item))))">
<ListItemComponent Id="@(IdSource(item))"
Name="@(NameSource(item))"
AdditionalNameInfo="@(AdditionalNameInfoSource(item))"
Rating="@(RatingSource(item))"
PictureDownloadingTask="@(PictureDownloadingTask)"/>
</div>
}
if (!_allItemsLoaded)
{
<div role="button" class="rounded-3 panel panel-regular p-3" @onclick="DownloadItems">
<div class="d-flex justify-content-center">
@if (!_itemsLoading)
{
<strong>Load more</strong>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<strong>Loading...</strong>
}
</div>
</div>
}
}
else
{
<ErrorComponent ErrorMessage="@_error"/>
}
}
else
{
<div class="m-5">
<LoadingComponent/>
</div>
}
</div>
</CascadingValue>

View File

@@ -0,0 +1,139 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model;
using WatchIt.Common.Model.Movies;
using WatchIt.Common.Model.Rating;
using WatchIt.Common.Query;
namespace WatchIt.Website.Components.DatabasePage;
public partial class DatabasePageComponent<TItem, TQuery> : ComponentBase where TItem : IQueryOrderable<TItem> where TQuery : QueryParameters<TItem>
{
#region SERVICES
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public required string Title { 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 Func<TItem, RatingResponse> RatingSource { get; set; }
[Parameter] public required string UrlIdTemplate { get; set; }
[Parameter] public required Func<long, Action<Picture>, Task> PictureDownloadingTask { get; set; }
[Parameter] public required Func<TQuery, Action<IEnumerable<TItem>>, Task> ItemDownloadingTask { get; set; }
[Parameter] public required Dictionary<string, string> SortingOptions { get; set; }
[Parameter] public required RenderFragment ChildContent { get; set; }
#endregion
#region FIELDS
private bool _loaded;
private string? _error;
private List<TItem> _items = new List<TItem>();
private bool _allItemsLoaded;
private bool _itemsLoading;
#endregion
#region PROPERTIES
public TQuery Query { get; set; } = Activator.CreateInstance<TQuery>()!;
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// INIT
Query.OrderBy = SortingOptions.Keys.First();
Query.First = 100;
List<Task> endTasks = new List<Task>();
// STEP 0
endTasks.AddRange(
[
ItemDownloadingTask(Query, data =>
{
_items.AddRange(data);
if (data.Count() < 100)
{
_allItemsLoaded = true;
}
else
{
Query.After = 100;
}
})
]);
// END
await Task.WhenAll(endTasks);
_loaded = true;
StateHasChanged();
}
}
private async Task DownloadItems()
{
_itemsLoading = true;
await ItemDownloadingTask(Query, AppendNewItems);
}
private async Task SortingAscendingChanged(ChangeEventArgs args)
{
Query.Order = (bool)args.Value! ? "asc" : "desc";
await UpdateItems();
}
private async Task SortingOptionChanged(ChangeEventArgs args)
{
Query.OrderBy = args.Value!.ToString();
await UpdateItems();
}
private async Task FilterApplied() => await UpdateItems();
private async Task UpdateItems()
{
_loaded = false;
Query.First = 100;
Query.After = null;
_items.Clear();
await ItemDownloadingTask(Query, AppendNewItems);
_loaded = true;
}
private void AppendNewItems(IEnumerable<TItem> items)
{
_items.AddRange(items);
if (items.Count() < 100)
{
_allItemsLoaded = true;
}
else
{
Query.After += 100;
}
_itemsLoading = false;
}
#endregion
}

View File

@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Query;
namespace WatchIt.Website.Components.DatabasePage;
public abstract class FilterFormComponent<TItem, TQuery> : ComponentBase where TItem : IQueryOrderable<TItem> where TQuery : QueryParameters<TItem>
{
#region PARAMETERS
[CascadingParameter]
protected DatabasePageComponent<TItem, TQuery> Parent { get; set; }
#endregion
#region FIELDS
protected TQuery? Query => Parent?.Query;
#endregion
}

View File

@@ -0,0 +1,66 @@
@inherits FilterFormComponent<WatchIt.Common.Model.Movies.MovieResponse, WatchIt.Common.Model.Movies.MovieQueryParameters>
<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">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">Budget</span>
<NumericEdit TValue="decimal?" Class="col form-control" Min="0" @bind-Value="@(Query.BudgetFrom)"/>
<span class="col-auto input-group-text">-</span>
<NumericEdit TValue="decimal?" Class="col form-control" Min="0" @bind-Value="@(Query.BudgetTo)"/>
</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>

View File

@@ -0,0 +1,69 @@
@inherits FilterFormComponent<WatchIt.Common.Model.Series.SeriesResponse, WatchIt.Common.Model.Series.SeriesQueryParameters>
<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">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">Has ended</span>
<div class="btn-group col">
<input type="radio" class="btn-check" name="has_ended" id="has_ended_yes" autocomplete="off" @onclick="() => Query.HasEnded = true">
<label class="btn btn-outline-secondary btn-sm" for="has_ended_yes">Yes</label>
<input type="radio" class="btn-check" name="has_ended" id="has_ended_no_choice" autocomplete="off" @onclick="() => Query.HasEnded = null" checked>
<label class="btn btn-outline-secondary btn-sm" for="has_ended_no_choice">No choice</label>
<input type="radio" class="btn-check" name="has_ended" id="has_ended_no" autocomplete="off" @onclick="() => Query.HasEnded = false">
<label class="btn btn-outline-secondary btn-sm" for="has_ended_no">No</label>
</div>
</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>

View File

@@ -1,32 +1,28 @@
<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="row">
<div class="container-fluid"> <div class="col">
<div class="row"> <div class="d-flex justify-content-center">
<div class="col"> <div class="text-danger icon-size">⚠&#xFE0E;</div>
<div class="d-flex justify-content-center">
<div class="text-danger icon-size">⚠&#xFE0E;</div>
</div>
</div>
</div> </div>
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
<h3 class="text-danger">An error occured while loading a page</h3>
</div>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
<p>@ErrorMessage</p>
</div>
</div>
</div>
}
</div> </div>
</div> </div>
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
<h3 class="text-danger">An error occured while loading a page</h3>
</div>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
<p>@ErrorMessage</p>
</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.Count > 0 ? Rating.Average : "--")/10</span>
@if (Rating.Count > 0)
{
<span id="ratingCount">@(Rating.Count)</span>
}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,54 @@
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 RatingResponse Rating { get; set; }
[Parameter] public required Func<long, Action<Picture>, Task> PictureDownloadingTask { get; set; }
[Parameter] public int PictureHeight { get; set; } = 150;
#endregion
#region FIELDS
private bool _loaded;
private Picture? _picture;
#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),
]);
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 justify-content-center">
<div class="d-flex flex-column m-5"> <div id="spinner" class="spinner-border text-@(Color)"></div>
<div class="d-flex justify-content-center"> </div>
<div id="spinner" class="spinner-border text-dark"></div> <div class="d-flex justify-content-center">
</div> <p id="text" class="text-@(Color)" m-0>Loading...</p>
<div class="d-flex justify-content-center">
<p id="text" class="text-dark">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]))"
Rating="@(RatingSource(_items[i]))"
PictureDownloadingTask="@(PictureDownloadingTask)"/>
</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>Loading...</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 Func<TItem, RatingResponse> RatingSource { get; set; }
[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; }
#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,175 +1,102 @@
@inherits LayoutComponentBase @using System.Net
@using WatchIt.Common.Model.Photos
@using WatchIt.Website.Services.WebAPI.Photos
@if (_loaded) @inherits LayoutComponentBase
{
<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="col-2"> <CascadingValue Value="this">
<a class="logo" href="/"> @if (_loaded)
WatchIt {
</a> <div class="container-xl">
</div> <div class="row sticky-top top-3 mb-2rem">
<div class="col"> <div class="col">
<p>Menu</p> <div class="panel panel-header rounded-3 px-3">
</div> <div class="container-grid">
<div class="col-auto"> <div class="row align-items-center">
<div class="d-flex flex-row-reverse"> <div class="col">
@if (_user is null) <a id="logo" class="logo" href="/">
{ WatchIt
<a class="main-button" href="/auth">Sign in</a> </a>
} </div>
else <div class="col-auto">
{ <div class="d-flex gap-2 align-items-center">
<div class="dropdown z-3"> @if (_searchbarVisible)
<a class="dropdown-toggle align-items-center text-decoration-none d-flex" id="dropdownUser" aria-expanded="false" @onclick="() => _userMenuIsActive = !_userMenuIsActive"> {
<img class="rounded-circle" alt="avatar" height="30" src="@(_userProfilePicture)"/> <div class="input-group input-group-sm">
<div class="text-decoration-none mx-2 text-white">@(_user.Username)</div> <InputText class="form-control" placeholder="Search with regex" @bind-Value="@(_searchbarText)"/>
</a> </div>
<ul class="dropdown-menu dropdown-menu-right text-small z-3" id="user-menu" aria-labelledby="dropdownUser"> <button type="button" class="btn btn-sm btn-outline-secondary" @onclick="@(SearchStart)">⌕</button>
<li> <button type="button" class="btn btn-sm" @onclick="@(() => _searchbarVisible = false)">&#10060;&#xFE0E;</button>
@if (_user.IsAdmin) }
{ else
<a class="dropdown-item" href="/admin">Administrator panel</a> {
} <Dropdown>
<div class="dropdown-menu-separator"></div> <DropdownToggle Color="Color.Default" Size="Size.Small" ToggleIconVisible="false">Database</DropdownToggle>
<a class="dropdown-item text-danger" @onclick="UserMenuLogOut">Log out</a> <DropdownMenu>
</li> <DropdownItem Clicked="@(() => NavigationManager.NavigateTo("/database/movies"))">Movies</DropdownItem>
</ul> <DropdownItem Clicked="@(() => NavigationManager.NavigateTo("/database/series"))">TV Series</DropdownItem>
</DropdownMenu>
</Dropdown>
<button type="button" class="btn btn-sm" @onclick="@(() => _searchbarVisible = true)">⌕</button>
}
</div>
</div>
<div class="col">
<div class="float-end">
@if (_user is null)
{
<a id="signInButton" class="main-button" href="/auth?redirect_to=@(WebUtility.UrlEncode(NavigationManager.Uri))">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)
{
<DropdownItem Clicked="@(() => NavigationManager.NavigateTo("/admin"))">Administrator panel</DropdownItem>
}
<DropdownDivider/>
<DropdownItem Clicked="UserMenuLogOut"><span class="text-danger">Log out</span></DropdownItem>
</DropdownMenu>
</Dropdown>
}
</div>
</div>
</div>
</div> </div>
} </div>
</div>
</div>
<div class="row">
<div class="col">
@Body
</div> </div>
</div> </div>
</div> </div>
<div class="row">
<div class="col z-0 p-1">
@Body
</div>
</div>
</div>
<style>
html {
height: 100%;
}
body {
background-image: url('@_background');
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 {
display: @(_userMenuIsActive ? "block" : "none");
position: fixed;
}
</style>
}
<style>
/* TAGS */
body {
@code background-image: url('@(GetBackgroundPhoto() is null ? "assets/background_temp.jpg": GetBackgroundPhoto().ToString())');
{
#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!;
#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());
} }
await Task.WhenAll(bgTasks);
_loaded = true;
StateHasChanged();
}
}
private async Task GetBackground() /* IDS */
{
Action<MediaPhotoResponse> 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 MediaWebAPIService.GetPhotoRandomBackground(backgroundSuccess);
}
private async Task GetAuthenticatedUser() #logo, #signInButton {
{ background-image: linear-gradient(45deg, @(GetBackgroundPhoto() is null ? "#c6721c, #85200c" : $"#{Convert.ToHexString(GetBackgroundPhoto().Background.FirstGradientColor)}, #{Convert.ToHexString(GetBackgroundPhoto().Background.SecondGradientColor)}"));
_user = await AuthenticationService.GetUserAsync(); }
</style>
} }
</CascadingValue>
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
{ {
<ErrorComponent ErrorMessage="You do not have permission to view this site"/> <div class="row">
<div class="col">
<ErrorComponent ErrorMessage="You do not have permission to view this site"/>
</div>
</div>
} }
} }
else else
{ {
<LoadingComponent/> <div class="row">
<div class="col">
<div class="m-5">
<LoadingComponent/>
</div>
</div>
</div>
} }
</div> </div>

View File

@@ -1,15 +1,24 @@
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!;
#endregion #endregion
#region PARAMETERS
[CascadingParameter] public MainLayout Layout { get; set; }
#endregion
@@ -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,117 +1,116 @@
@page "/auth" @page "/auth"
@layout EmptyLayout @layout EmptyLayout
<PageTitle>WatchIt - @(_authType == AuthType.SignIn ? "Sign in" : "Sign up")</PageTitle> <PageTitle>WatchIt - @(_isSingUp ? "Sign up" : "Sign in")</PageTitle>
@if (_loaded) @if (_loaded)
{ {
<div class="h-100 d-flex align-items-center justify-content-center"> <div class="h-100 d-flex align-items-center justify-content-center">
<div class="d-inline-flex flex-column justify-content-center panel panel-header rounded-3"> <div class="panel panel-header rounded-3 p-3">
<a class="logo" href="/"> <div class="d-flex flex-column align-items-center gap-3">
WatchIt <a id="logo" class="logo m-0" href="/">WatchIt</a>
</a> @if (_isSingUp)
<div>
@if (_authType == AuthType.SignIn)
{ {
<form method="post" @onsubmit="Login" @formname="login"> <EditForm Model="@(_registerModel)">
<AntiforgeryToken/> <AntiforgeryToken/>
<div> <div class="container-grid">
<label> <div class="row form-group mb-1">
Username or email: <label for="username" class="col-5 col-form-label">Username:</label>
<InputText @bind-Value="_loginModel!.UsernameOrEmail"/> <div class="col">
</label> <InputText id="username" class="form-control" @bind-Value="_registerModel!.Username"/>
</div>
</div>
<div class="row form-group my-1">
<label for="email" class="col-5 col-form-label">Email:</label>
<div class="col">
<InputText id="email" class="form-control" @bind-Value="_registerModel!.Email"/>
</div>
</div>
<div class="row form-group my-1">
<label for="password" class="col-5 col-form-label">Password:</label>
<div class="col">
<InputText id="password" class="form-control" type="password" @bind-Value="_registerModel!.Password"/>
</div>
</div>
<div class="row form-group my-1">
<label for="confpassword" class="col-5 col-form-label">Confirm password:</label>
<div class="col">
<InputText id="confpassword" class="form-control" type="password" @bind-Value="_registerPasswordConfirmation"/>
</div>
</div>
<div class="row">
<div class="col align-self-center">
<span class="text-@(_formMessageIsSuccess ? "success" : "danger")">@_formMessage</span>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-dark" @onclick="@(Register)">Sign up</button>
</div>
</div>
</div> </div>
<div> </EditForm>
<label>
Password:
<InputText type="password" @bind-Value="_loginModel!.Password"/>
</label>
</div>
<div>
<label>
<InputCheckbox @bind-Value="_loginModel!.RememberMe"></InputCheckbox>
Remember me
</label>
</div>
<div>
<button type="submit">Sign in</button>
</div>
</form>
} }
else else
{ {
<form method="post" @onsubmit="Register" @formname="register"> <EditForm Model="@(_loginModel)">
<AntiforgeryToken/> <AntiforgeryToken/>
<div> <div class="container-grid">
<label> <div class="row form-group mb-1">
Username: <label for="username" class="col-5 col-form-label">Username or email:</label>
<InputText @bind-Value="_registerModel!.Username"/> <div class="col">
</label> <InputText id="username" class="form-control" @bind-Value="_loginModel!.UsernameOrEmail"/>
</div>
</div>
<div class="row form-group my-1">
<label for="password" class="col-5 col-form-label">Password:</label>
<div class="col">
<InputText id="password" type="password" class="form-control" @bind-Value="_loginModel!.Password"/>
</div>
</div>
<div class="row form-group">
<div class="col">
<div class="form-check">
<InputCheckbox class="form-check-input" @bind-Value="_loginModel!.RememberMe"/>
<label class="form-check-label">Remember me</label>
</div>
</div>
</div>
<div class="row">
<div class="col align-self-center">
<span class="text-@(_formMessageIsSuccess ? "success" : "danger")">@_formMessage</span>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-dark" @onclick="@(Login)">Sign in</button>
</div>
</div>
</div> </div>
<div> </EditForm>
<label>
Email:
<InputText @bind-Value="_registerModel!.Email"/>
</label>
</div>
<div>
<label>
Password:
<InputText type="password" @bind-Value="_registerModel!.Password"/>
</label>
</div>
<div>
<label>
Confirm password:
<InputText type="password" @bind-Value="_passwordConfirmation"/>
</label>
</div>
<div>
<button type="submit">Sign up</button>
</div>
</form>
} }
</div> <div class="btn-group w-100">
@if (_errors is not null) <input type="radio" class="btn-check" name="signtype" id="signin" autocomplete="off" checked="@(!_isSingUp)" @onclick="() => { _isSingUp = false; _formMessage = null; _formMessageIsSuccess = false; }">
{ <label class="btn btn-outline-secondary btn-sm" for="signin">Sign in</label>
<div class="text-danger"> <input type="radio" class="btn-check" name="signtype" id="signup" autocomplete="off" checked="@(_isSingUp)" @onclick="() => { _isSingUp = true; _formMessage = null; _formMessageIsSuccess = false; }">
@foreach (string error in _errors) <label class="btn btn-outline-secondary btn-sm" for="signup">Sign up</label>
{
@error
<br/>
}
</div> </div>
}
<div>
<label>
<input type="radio" checked="@(() => _authType == AuthType.SignIn)" name="auth" @onchange="@(() => _authType = AuthType.SignIn)" />
Sign in
</label>
<label>
<input type="radio" checked="@(() => _authType == AuthType.SignUp)" name="auth" @onchange="@(() => _authType = AuthType.SignUp)" />
Sign up
</label>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
/* TAGS */
body { body {
height: 100%; background-image: url('@(_background is null ? "assets/background_temp.jpg": _background.ToString())');
background-image: url('@_background');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
} }
.logo {
background-image: linear-gradient(45deg, @_firstGradientColor, @_secondGradientColor); /* IDS */
#logo {
background-image: linear-gradient(45deg, @(_background is null ? "#c6721c, #85200c" : $"#{Convert.ToHexString(_background.Background.FirstGradientColor)}, #{Convert.ToHexString(_background.Background.SecondGradientColor)}"));
} }
</style> </style>
} }

View File

@@ -1,10 +1,13 @@
using Microsoft.AspNetCore.Components; using System.Net;
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Accounts; using WatchIt.Common.Model.Accounts;
using WatchIt.Common.Model.Media; using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Photos;
using WatchIt.Website.Services.Utility.Authentication; using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.Utility.Tokens; using WatchIt.Website.Services.Utility.Tokens;
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 WatchIt.Website.Services.WebAPI.Photos;
namespace WatchIt.Website.Pages; namespace WatchIt.Website.Pages;
@@ -17,48 +20,45 @@ public partial class AuthPage
[Inject] public ITokensService TokensService { get; set; } = default!; [Inject] public ITokensService TokensService { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!; [Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IAccountsWebAPIService AccountsWebAPIService { get; set; } = default!; [Inject] public IAccountsWebAPIService AccountsWebAPIService { get; set; } = default!;
[Inject] public IPhotosWebAPIService PhotosWebAPIService { get; set; } = default!;
[Inject] public NavigationManager NavigationManager { get; set; } = default!; [Inject] public NavigationManager NavigationManager { get; set; } = default!;
#endregion #endregion
#region ENUMS #region PARAMETERS
private enum AuthType
{
SignIn,
SignUp
}
[SupplyParameterFromQuery(Name = "redirect_to")]
private string RedirectTo { get; set; } = "/";
#endregion #endregion
#region FIELDS #region FIELDS
private bool _loaded = false; private bool _loaded;
private AuthType _authType = AuthType.SignIn; private PhotoResponse? _background;
private string _background = "assets/background_temp.jpg";
private string _firstGradientColor = "#c6721c";
private string _secondGradientColor = "#85200c";
private AuthenticateRequest _loginModel = new AuthenticateRequest
{
UsernameOrEmail = null,
Password = null
};
private bool _isSingUp;
private string? _formMessage;
private bool _formMessageIsSuccess;
private RegisterRequest _registerModel = new RegisterRequest private RegisterRequest _registerModel = new RegisterRequest
{ {
Username = null, Username = null,
Email = null, Email = null,
Password = null Password = null
}; };
private string _passwordConfirmation; private string _registerPasswordConfirmation;
private IEnumerable<string> _errors; private AuthenticateRequest _loginModel = new AuthenticateRequest
{
UsernameOrEmail = null,
Password = null
};
#endregion #endregion
@@ -72,22 +72,19 @@ public partial class AuthPage
{ {
if (await AuthenticationService.GetAuthenticationStatusAsync()) if (await AuthenticationService.GetAuthenticationStatusAsync())
{ {
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo(WebUtility.UrlDecode(RedirectTo));
} }
Action<MediaPhotoResponse> 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}"; List<Task> endTasks = new List<Task>();
_firstGradientColor = $"#{firstColor}";
_secondGradientColor = $"#{secondColor}"; // STEP 0
}; endTasks.AddRange(
await MediaWebAPIService.GetPhotoRandomBackground(backgroundSuccess); [
PhotosWebAPIService.GetPhotoRandomBackground(data => _background = data)
]);
// END
await Task.WhenAll(endTasks);
_loaded = true; _loaded = true;
StateHasChanged(); StateHasChanged();
@@ -96,44 +93,51 @@ public partial class AuthPage
private async Task Login() private async Task Login()
{ {
await AccountsWebAPIService.Authenticate(_loginModel, LoginSuccess, LoginBadRequest, LoginUnauthorized);
async void LoginSuccess(AuthenticateResponse data)
{
await TokensService.SaveAuthenticationData(data);
NavigationManager.NavigateTo("/");
}
void LoginBadRequest(IDictionary<string, string[]> data) void LoginBadRequest(IDictionary<string, string[]> data)
{ {
_errors = data.SelectMany(x => x.Value).Select(x => $"• {x}"); _formMessageIsSuccess = false;
_formMessage = data.SelectMany(x => x.Value).FirstOrDefault();
} }
void LoginUnauthorized() void LoginUnauthorized()
{ {
_errors = [ "Incorrect account data" ]; _formMessageIsSuccess = false;
_formMessage = "Incorrect account data";
} }
async Task LoginSuccess(AuthenticateResponse data)
{
await TokensService.SaveAuthenticationData(data);
NavigationManager.NavigateTo(RedirectTo);
}
await AccountsWebAPIService.Authenticate(_loginModel, async (data) => await LoginSuccess(data), LoginBadRequest, LoginUnauthorized);
} }
private async Task Register() private async Task Register()
{ {
if (_registerModel.Password != _passwordConfirmation)
{
_errors = [ "Password fields don't match" ];
return;
}
await AccountsWebAPIService.Register(_registerModel, RegisterSuccess, RegisterBadRequest);
void RegisterSuccess(RegisterResponse data) void RegisterSuccess(RegisterResponse data)
{ {
_authType = AuthType.SignIn; _formMessageIsSuccess = true;
_formMessage = "You are registered. You can sign in now.";
_isSingUp = false;
} }
void RegisterBadRequest(IDictionary<string, string[]> data) void RegisterBadRequest(IDictionary<string, string[]> data)
{ {
_errors = data.SelectMany(x => x.Value).Select(x => $"• {x}"); _formMessageIsSuccess = false;
_formMessage = data.SelectMany(x => x.Value).FirstOrDefault();
} }
if (_registerModel.Password != _registerPasswordConfirmation)
{
_formMessageIsSuccess = false;
_formMessage = "Password fields don't match";
return;
}
await AccountsWebAPIService.Register(_registerModel, RegisterSuccess, RegisterBadRequest);
} }
#endregion #endregion

View File

@@ -4,6 +4,14 @@ html {
height: 100%; height: 100%;
} }
body {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
height: 100%;
}
/* CLASSES */ /* CLASSES */

View File

@@ -0,0 +1,63 @@
@using WatchIt.Common.Model.Movies
@using WatchIt.Common.Model.Series
@using WatchIt.Website.Components.DatabasePage
@page "/database/{type?}"
@if (_loaded)
{
switch (Type)
{
case "movies":
<DatabasePageComponent TItem="MovieResponse"
TQuery="MovieQueryParameters"
Title="Movies database"
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) => MediaWebAPIService.GetMediaPoster(id, action))"
ItemDownloadingTask="@(MoviesWebAPIService.GetAllMovies)"
SortingOptions="@(new Dictionary<string, string>
{
{ "rating.count", "Number of ratings" },
{ "rating.average", "Average rating" },
{ "title", "Title" },
{ "release_date", "Release date" },
})">
<MoviesFilterFormComponent/>
</DatabasePageComponent>
break;
case "series":
<DatabasePageComponent TItem="SeriesResponse"
TQuery="SeriesQueryParameters"
Title="TV series database"
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) => MediaWebAPIService.GetMediaPoster(id, action))"
ItemDownloadingTask="@(SeriesWebAPIService.GetAllSeries)"
SortingOptions="@(new Dictionary<string, string>
{
{ "rating.count", "Number of ratings" },
{ "rating.average", "Average rating" },
{ "title", "Title" },
{ "release_date", "Release date" },
})">
<SeriesFilterFormComponent/>
</DatabasePageComponent>
break;
}
}
else
{
<div class="m-5">
<LoadingComponent/>
</div>
}

View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Website.Components.DatabasePage;
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 DatabasePage : ComponentBase
{
#region SERVICES
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] private IMoviesWebAPIService MoviesWebAPIService { get; set; } = default!;
[Inject] private ISeriesWebAPIService SeriesWebAPIService { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public string? Type { get; set; }
#endregion
#region FIELDS
private static IEnumerable<string> _databaseTypes = ["movies", "series"];
private bool _loaded;
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// INIT
if (!_databaseTypes.Contains(Type))
{
NavigationManager.NavigateTo($"/database/{_databaseTypes.First()}");
}
_loaded = true;
StateHasChanged();
}
}
#endregion
}

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
{ {
<ErrorComponent ErrorMessage="@_error"/> <div class="row">
<div class="col">
<ErrorComponent ErrorMessage="@_error"/>
</div>
</div>
} }
} }
else else
{ {
<LoadingComponent/> <div class="row">
<div class="col">
<div class="m-5">
<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>();
@@ -53,8 +64,8 @@ public partial class HomePage
await Task.WhenAll(step1Tasks); await Task.WhenAll(step1Tasks);
endTasks.AddRange( endTasks.AddRange(
[ [
Parallel.ForEachAsync(_topMovies, async (x, _) => await MediaWebAPIService.GetPoster(x.Key.Id, y => _topMovies[x.Key] = y)), Parallel.ForEachAsync(_topMovies, async (x, _) => await MediaWebAPIService.GetMediaPoster(x.Key.Id, y => _topMovies[x.Key] = y)),
Parallel.ForEachAsync(_topSeries, async (x, _) => await MediaWebAPIService.GetPoster(x.Key.Id, y => _topSeries[x.Key] = y)) Parallel.ForEachAsync(_topSeries, async (x, _) => await MediaWebAPIService.GetMediaPoster(x.Key.Id, y => _topSeries[x.Key] = y))
]); ]);
// END // END

View File

@@ -1,8 +1,10 @@
@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"
@page "/media/new/{type}" @page "/media/new/{type?}"
<PageTitle> <PageTitle>
@@ -38,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))
@@ -48,14 +50,14 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="rounded-3 panel panel-regular p-2"> <div class="rounded-3 panel panel-regular p-2">
<h2 class="m-0 mx-2 mb-1 p-0">@(_media is not null ? "Edit" : "Create new") @(_movieRequest is not null ? "movie" : "series")@(_media is not null ? $" \"{_media.Title}\"" : string.Empty)</h2> <h3 class="m-0 mx-2 mb-1 p-0">@(_media is not null ? "Edit" : "Create new") @(_movieRequest is not null ? "movie" : "series")@(_media is not null ? $" \"{_media.Title}\"" : string.Empty)</h3>
</div> </div>
</div> </div>
</div> </div>
<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"/>
@@ -75,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>
@@ -94,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>
@@ -113,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">
@@ -181,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>
@@ -197,34 +199,193 @@
</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-grid">
<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>Save</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>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-grid 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-grid">
<div class="row">
<div class="col-auto">
<div class="container-grid">
<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-grid">
<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
{ {
<ErrorComponent ErrorMessage="You do not have permission to view this site"/> <div class="row">
<div class="col">
<ErrorComponent ErrorMessage="You do not have permission to view this site"/>
</div>
</div>
} }
} }
else else
{ {
<ErrorComponent ErrorMessage="@_error"/> <div class="row">
<div class="col">
<ErrorComponent ErrorMessage="@_error"/>
</div>
</div>
} }
} }
else else
{ {
<LoadingComponent/> <div class="row">
<div class="col">
<div class="m-5">
<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

@@ -3,10 +3,13 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
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.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;
using WatchIt.Website.Services.WebAPI.Photos;
using WatchIt.Website.Services.WebAPI.Series; using WatchIt.Website.Services.WebAPI.Series;
namespace WatchIt.Website.Pages; namespace WatchIt.Website.Pages;
@@ -20,6 +23,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
@@ -30,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
@@ -40,8 +46,6 @@ public partial class MediaEditPage : ComponentBase
private string? _error; private string? _error;
private User? _user; private User? _user;
private MediaPhotoResponse? _background;
private MediaResponse? _media; private MediaResponse? _media;
private MovieRequest? _movieRequest; private MovieRequest? _movieRequest;
@@ -55,6 +59,21 @@ public partial class MediaEditPage : ComponentBase
private bool _mediaPosterChanged; private bool _mediaPosterChanged;
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
@@ -68,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>();
@@ -94,12 +115,13 @@ public partial class MediaEditPage : ComponentBase
{ {
endTasks.AddRange( endTasks.AddRange(
[ [
MediaWebAPIService.GetPhotoMediaRandomBackground(Id.Value, data => _background = data), MediaWebAPIService.GetMediaPhotoRandomBackground(Id.Value, data => Layout.BackgroundPhoto = data),
MediaWebAPIService.GetPoster(Id.Value, data => MediaWebAPIService.GetMediaPoster(Id.Value, data =>
{ {
_mediaPosterSaved = data; _mediaPosterSaved = data;
_mediaPosterRequest = new MediaPosterRequest(data); _mediaPosterRequest = new MediaPosterRequest(data);
}) }),
MediaWebAPIService.GetMediaPhotos(Id.Value, successAction: data => _photos = data)
]); ]);
} }
@@ -179,7 +201,7 @@ public partial class MediaEditPage : ComponentBase
} }
_mediaPosterSaving = true; _mediaPosterSaving = true;
await MediaWebAPIService.PutPoster(Id.Value, _mediaPosterRequest, Success); await MediaWebAPIService.PutMediaPoster(Id.Value, _mediaPosterRequest, Success);
} }
private void CancelPoster() private void CancelPoster()
@@ -199,7 +221,7 @@ public partial class MediaEditPage : ComponentBase
} }
_mediaPosterDeleting = true; _mediaPosterDeleting = true;
await MediaWebAPIService.DeletePoster(Id.Value, Success); await MediaWebAPIService.DeleteMediaPoster(Id.Value, Success);
} }
#endregion #endregion
@@ -250,5 +272,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

@@ -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,11 +133,11 @@ 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">
<strong>Global rating:</strong> @(_globalRating.RatingCount == 0 ? "no ratings" : $"{Math.Round(_globalRating.RatingAverage, 1)}/10") <strong>Global rating:</strong> @(_globalRating.Count == 0 ? "no ratings" : $"{Math.Round(_globalRating.Average, 1)}/10")
</h4> </h4>
</div> </div>
</div> </div>
@@ -193,26 +193,21 @@ else
} }
else else
{ {
<ErrorComponent ErrorMessage="@_error"/> <div class="row">
<div class="col">
<ErrorComponent ErrorMessage="@_error"/>
</div>
</div>
} }
} }
else else
{ {
<LoadingComponent/> <div class="row">
<div class="col">
<div class="m-5">
<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

@@ -3,7 +3,10 @@ using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Genres; 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.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;
@@ -29,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
@@ -42,10 +47,9 @@ public partial class MediaPage : ComponentBase
private User? _user; private User? _user;
private MediaPhotoResponse? _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;
@@ -61,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>();
@@ -83,8 +89,8 @@ public partial class MediaPage : ComponentBase
endTasks.AddRange( endTasks.AddRange(
[ [
MediaWebAPIService.PostMediaView(Id), MediaWebAPIService.PostMediaView(Id),
MediaWebAPIService.GetPhotoMediaRandomBackground(Id, data => _background = data), MediaWebAPIService.GetMediaPhotoRandomBackground(Id, data => Layout.BackgroundPhoto = data),
MediaWebAPIService.GetPoster(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),
_media.Type == MediaType.Movie ? MoviesWebAPIService.GetMovie(Id, data => _movie = data) : SeriesWebAPIService.GetSeries(Id, data => _series = data), _media.Type == MediaType.Movie ? MoviesWebAPIService.GetMovie(Id, data => _movie = data) : SeriesWebAPIService.GetSeries(Id, data => _series = data),
@@ -118,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)"
RatingSource="@(item => item.Rating)"
Query="@(new MovieQueryParameters { Title = DecodedQuery, OrderBy = "rating.count" })"
ItemDownloadingTask="@(MoviesWebAPIService.GetAllMovies)"
PictureDownloadingTask="@((id, action) => MediaWebAPIService.GetMediaPoster(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)"
RatingSource="@(item => item.Rating)"
Query="@(new SeriesQueryParameters { Title = DecodedQuery, OrderBy = "rating.count" })"
ItemDownloadingTask="@(SeriesWebAPIService.GetAllSeries)"
PictureDownloadingTask="@((id, action) => MediaWebAPIService.GetMediaPoster(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;
@@ -8,6 +11,7 @@ using WatchIt.Website.Services.Utility.Tokens;
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 WatchIt.Website.Services.WebAPI.Movies; using WatchIt.Website.Services.WebAPI.Movies;
using WatchIt.Website.Services.WebAPI.Photos;
using WatchIt.Website.Services.WebAPI.Series; using WatchIt.Website.Services.WebAPI.Series;
namespace WatchIt.Website; namespace WatchIt.Website;
@@ -52,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>();
@@ -64,6 +74,7 @@ public static class Program
builder.Services.AddSingleton<IMediaWebAPIService, MediaWebAPIService>(); builder.Services.AddSingleton<IMediaWebAPIService, MediaWebAPIService>();
builder.Services.AddSingleton<IMoviesWebAPIService, MoviesWebAPIService>(); builder.Services.AddSingleton<IMoviesWebAPIService, MoviesWebAPIService>();
builder.Services.AddSingleton<ISeriesWebAPIService, SeriesWebAPIService>(); builder.Services.AddSingleton<ISeriesWebAPIService, SeriesWebAPIService>();
builder.Services.AddSingleton<IPhotosWebAPIService, PhotosWebAPIService>();
return builder; return builder;
} }

View File

@@ -22,6 +22,7 @@
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Accounts\WatchIt.Website.Services.WebAPI.Accounts.csproj" /> <ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Accounts\WatchIt.Website.Services.WebAPI.Accounts.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Media\WatchIt.Website.Services.WebAPI.Media.csproj" /> <ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Media\WatchIt.Website.Services.WebAPI.Media.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Movies\WatchIt.Website.Services.WebAPI.Movies.csproj" /> <ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Movies\WatchIt.Website.Services.WebAPI.Movies.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Photos\WatchIt.Website.Services.WebAPI.Photos.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Series\WatchIt.Website.Services.WebAPI.Series.csproj" /> <ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Series\WatchIt.Website.Services.WebAPI.Series.csproj" />
</ItemGroup> </ItemGroup>
@@ -34,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

@@ -14,4 +14,6 @@
@using WatchIt.Website.Services.Utility.Tokens @using WatchIt.Website.Services.Utility.Tokens
@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
@using Blazorise.Bootstrap5

View File

@@ -30,26 +30,21 @@
}, },
"Media": { "Media": {
"Base": "/media", "Base": "/media",
"Get": "/{0}", "GetMedia": "/{0}",
"GetGenres": "/{0}/genres", "GetMediaGenres": "/{0}/genres",
"PostGenre": "/{0}/genres/{1}", "PostMediaGenre": "/{0}/genres/{1}",
"DeleteGenre": "/{0}/genres/{1}", "DeleteMediaGenre": "/{0}/genres/{1}",
"GetMediaRating": "/{0}/rating", "GetMediaRating": "/{0}/rating",
"GetMediaRatingByUser": "/{0}/rating/{1}", "GetMediaRatingByUser": "/{0}/rating/{1}",
"PutMediaRating": "/{0}/rating", "PutMediaRating": "/{0}/rating",
"DeleteMediaRating": "/{0}/rating", "DeleteMediaRating": "/{0}/rating",
"PostMediaView": "/{0}/view", "PostMediaView": "/{0}/view",
"GetMediaPoster": "/{0}/poster",
"GetPhotoMediaRandomBackground": "/{0}/photos/random_background", "PutMediaPoster": "/{0}/poster",
"GetPoster": "/{0}/poster", "DeleteMediaPoster": "/{0}/poster",
"PutPoster": "/{0}/poster", "GetMediaPhotos": "/{0}/photos",
"DeletePoster": "/{0}/poster", "GetMediaPhotoRandomBackground": "/{0}/photos/random_background",
"GetPhoto": "/photos/{0}", "PostMediaPhoto": "/{0}/photos"
"GetPhotos": "/photos",
"GetPhotoRandomBackground": "/photos/random_background",
"PostPhoto": "/photos",
"PutPhoto": "/photos/{0}",
"DeletePhoto": "/photos/{0}"
}, },
"Movies": { "Movies": {
"Base": "/movies", "Base": "/movies",
@@ -68,6 +63,13 @@
"PutSeries": "/{0}", "PutSeries": "/{0}",
"DeleteSeries": "/{0}", "DeleteSeries": "/{0}",
"GetSeriesViewRank": "/view" "GetSeriesViewRank": "/view"
},
"Photos": {
"Base": "/photos",
"GetPhotoRandomBackground": "/random_background",
"DeletePhoto": "/{0}",
"PutPhotoBackgroundData": "/{0}/background_data",
"DeletePhotoBackgroundData": "/{0}/background_data"
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 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

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;
@@ -83,13 +47,4 @@ 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%;
} }

Some files were not shown because too many files have changed in this diff Show More