Compare commits
15 Commits
d6a4015c91
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 53f1df780c | |||
| 53ec67f99d | |||
| 0ed3bc6298 | |||
| 8a13f282b1 | |||
| 5c27fb7f21 | |||
| 06d5aa345d | |||
| 5eecba7eec | |||
| 9c9e33ebec | |||
| 2bd7e3b970 | |||
| abcc82437f | |||
| f6674e0382 | |||
| 77a02a543d | |||
| 2355d7fe65 | |||
| 204e186354 | |||
| dbed9fc905 |
@@ -8,11 +8,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -22,7 +22,10 @@ public static class ServiceCollectionExtensions
|
||||
?? throw new InvalidOperationException("Missing ConnectionStrings:AppDb");
|
||||
|
||||
services.AddDbContextFactory<AppDbContext>(opt =>
|
||||
opt.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
|
||||
opt.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mySqlOptions =>
|
||||
{
|
||||
mySqlOptions.CommandTimeout(120);
|
||||
})
|
||||
.EnableSensitiveDataLogging(false));
|
||||
|
||||
services.AddControllers();
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
|
||||
using JSMR.Application.Creators.Queries.Search;
|
||||
using JSMR.Application.Tags.Commands.SetEnglishName;
|
||||
using JSMR.Application.Tags.Commands.UpdateTagStatus;
|
||||
using JSMR.Application.Tags.Queries.Search;
|
||||
using JSMR.Application.Users;
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
@@ -48,6 +53,23 @@ public static class WebApplicationExtensions
|
||||
}
|
||||
});
|
||||
|
||||
app.UseExceptionHandler(errorApp =>
|
||||
{
|
||||
errorApp.Run(async context =>
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
context.Response.ContentType = "application/problem+json";
|
||||
|
||||
await Results.Problem(
|
||||
title: "An unexpected error occurred.",
|
||||
detail: app.Environment.IsDevelopment()
|
||||
? "Check the API logs for details."
|
||||
: null,
|
||||
statusCode: StatusCodes.Status500InternalServerError
|
||||
).ExecuteAsync(context);
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -56,6 +78,9 @@ public static class WebApplicationExtensions
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||
|
||||
app.MapSearchEndpoints();
|
||||
app.MapVoiceWorkCommandEndpoints();
|
||||
app.MapTagCommandEndpoints();
|
||||
app.MapCreatorCommandEndpoints();
|
||||
app.MapAuthenticationEndpoints();
|
||||
}
|
||||
|
||||
@@ -105,6 +130,60 @@ public static class WebApplicationExtensions
|
||||
});
|
||||
}
|
||||
|
||||
private static void MapVoiceWorkCommandEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapPost("/api/voicework/set-favorite", async (
|
||||
SetVoiceWorkFavoriteRequest request,
|
||||
SetVoiceWorkFavoriteHandler handler,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var result = await handler.HandleAsync(request, ct);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
app.MapPost("/api/voicework/delete", async (
|
||||
DeleteVoiceWorkRequest request,
|
||||
DeleteVoiceWorkHandler handler,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var result = await handler.HandleAsync(request, ct);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
}
|
||||
|
||||
private static void MapTagCommandEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapPost("/api/tags/update-status", async (
|
||||
UpdateTagStatusRequest request,
|
||||
UpdateTagStatusHandler handler,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var result = await handler.HandleAsync(request, ct);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
app.MapPost("/api/tags/set-english-name", async (
|
||||
SetTagEnglishNameRequest request,
|
||||
SetTagEnglishNameHandler handler,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var result = await handler.HandleAsync(request, ct);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
}
|
||||
|
||||
private static void MapCreatorCommandEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapPost("/api/creators/update-status", async (
|
||||
UpdateCreatorStatusRequest request,
|
||||
UpdateCreatorStatusHandler handler,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var result = await handler.HandleAsync(request, ct);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
}
|
||||
|
||||
private static void MapAuthenticationEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapPost("/auth/login", async (LoginRequest req, IUserRepository users, HttpContext http) =>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
public record CreatorSearchItem
|
||||
{
|
||||
public int CreatorId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public bool Favorite { get; init; }
|
||||
public bool Blacklisted { get; init; }
|
||||
public int VoiceWorkCount { get; init; }
|
||||
public int CreatorId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool Blacklisted { get; set; }
|
||||
public int VoiceWorkCount { get; set; }
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
|
||||
using JSMR.Application.Creators.Queries.Search;
|
||||
using JSMR.Application.Scanning;
|
||||
using JSMR.Application.Tags.Commands.SetEnglishName;
|
||||
using JSMR.Application.Tags.Commands.UpdateTagStatus;
|
||||
using JSMR.Application.Tags.Queries.Search;
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -17,6 +20,9 @@ public static class ApplicationServiceCollectionExtensions
|
||||
services.AddScoped<SearchCirclesHandler>();
|
||||
|
||||
services.AddScoped<SearchVoiceWorksHandler>();
|
||||
services.AddScoped<SetVoiceWorkFavoriteHandler>();
|
||||
services.AddScoped<DeleteVoiceWorkHandler>();
|
||||
|
||||
services.AddScoped<ScanVoiceWorksHandler>();
|
||||
|
||||
services.AddScoped<SearchTagsHandler>();
|
||||
@@ -24,6 +30,7 @@ public static class ApplicationServiceCollectionExtensions
|
||||
services.AddScoped<UpdateTagStatusHandler>();
|
||||
|
||||
services.AddScoped<SearchCreatorsHandler>();
|
||||
services.AddScoped<UpdateCreatorStatusHandler>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,11 +4,8 @@ namespace JSMR.Application.Scanning.Contracts;
|
||||
|
||||
public class DLSiteWork
|
||||
{
|
||||
//public DLSiteWorkType Type { get; set; }
|
||||
//public DLSiteWorkCategory Category { get; set; }
|
||||
public required string ProductName { get; set; }
|
||||
public required string ProductId { get; set; }
|
||||
//public DateOnly? AnnouncedDate { get; set; }
|
||||
public DateOnly? ExpectedDate { get; set; }
|
||||
public DateOnly? SalesDate { get; set; }
|
||||
public int Downloads { get; set; }
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
public record TagSearchItem
|
||||
{
|
||||
public int TagId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public bool Favorite { get; init; }
|
||||
public bool Blacklisted { get; init; }
|
||||
public string? EnglishName { get; init; }
|
||||
public int VoiceWorkCount { get; init; }
|
||||
public int TagId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool Blacklisted { get; set; }
|
||||
public string? EnglishName { get; set; }
|
||||
public int VoiceWorkCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using JSMR.Application.VoiceWorks.Ports;
|
||||
|
||||
namespace JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
|
||||
public sealed class DeleteVoiceWorkHandler(IVoiceWorkWriter writer)
|
||||
{
|
||||
public async Task<DeleteVoiceWorkResponse> HandleAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await writer.DeleteAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
|
||||
public sealed record DeleteVoiceWorkRequest(int[] VoiceWorkIds);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
|
||||
public sealed record DeleteVoiceWorkResponse(Dictionary<int, DeleteVoiceWorkStatus> Results);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
|
||||
public enum DeleteVoiceWorkStatus
|
||||
{
|
||||
Deleted,
|
||||
NotFound,
|
||||
NotAllowed,
|
||||
Failed
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
|
||||
namespace JSMR.Application.VoiceWorks.Ports;
|
||||
|
||||
public interface IVoiceWorkWriter
|
||||
{
|
||||
Task<SetVoiceWorkFavoriteResponse> SetFavoriteAsync(SetVoiceWorkFavoriteRequest request, CancellationToken cancellationToken);
|
||||
Task<DeleteVoiceWorkResponse> DeleteAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -4,29 +4,29 @@ namespace JSMR.Application.VoiceWorks.Queries.Search;
|
||||
|
||||
public record VoiceWorkSearchResult
|
||||
{
|
||||
public int VoiceWorkId { get; init; }
|
||||
public required string ProductId { get; init; }
|
||||
public string? OriginalProductId { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required string ProductName { get; init; }
|
||||
public required string ProductUrl { get; init; }
|
||||
public bool HasImage { get; init; }
|
||||
public required string Maker { get; init; }
|
||||
public required string MakerId { get; init; }
|
||||
public DateTime? ExpectedDate { get; init; }
|
||||
public DateTime? SalesDate { get; init; }
|
||||
public DateTime? PlannedReleaseDate { get; init; }
|
||||
public int? Downloads { get; init; }
|
||||
public int? WishlistCount { get; init; }
|
||||
public byte? StarRating { get; init; }
|
||||
public int? Votes { get; init; }
|
||||
public bool HasTrial { get; init; }
|
||||
public bool HasChobit { get; init; }
|
||||
public AgeRating Rating { get; init; }
|
||||
public bool Favorite { get; init; }
|
||||
public byte Status { get; init; }
|
||||
public byte SubtitleLanguage { get; init; }
|
||||
public bool? IsValid { get; init; }
|
||||
public int VoiceWorkId { get; set; }
|
||||
public required string ProductId { get; set; }
|
||||
public string? OriginalProductId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public required string ProductName { get; set; }
|
||||
public required string ProductUrl { get; set; }
|
||||
public bool HasImage { get; set; }
|
||||
public required string Maker { get; set; }
|
||||
public required string MakerId { get; set; }
|
||||
public DateTime? ExpectedDate { get; set; }
|
||||
public DateTime? SalesDate { get; set; }
|
||||
public DateTime? PlannedReleaseDate { get; set; }
|
||||
public int? Downloads { get; set; }
|
||||
public int? WishlistCount { get; set; }
|
||||
public byte? StarRating { get; set; }
|
||||
public int? Votes { get; set; }
|
||||
public bool HasTrial { get; set; }
|
||||
public bool HasChobit { get; set; }
|
||||
public AgeRating Rating { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public byte Status { get; set; }
|
||||
public byte SubtitleLanguage { get; set; }
|
||||
public bool? IsValid { get; set; }
|
||||
public required VoiceWorkCircleItem Circle { get; set; }
|
||||
public VoiceWorkCircleItem? OriginalCircle { get; set; }
|
||||
public VoiceWorkTagItem[] Tags { get; set; } = [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Ports;
|
||||
using JSMR.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -17,6 +18,57 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
|
||||
return new SetVoiceWorkFavoriteResponse(request.VoiceWorkId, request.IsFavorite);
|
||||
}
|
||||
|
||||
public async Task<DeleteVoiceWorkResponse> DeleteAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Dictionary<int, DeleteVoiceWorkStatus> results = request.VoiceWorkIds.Select(x => x)
|
||||
.ToDictionary(x => x, x => DeleteVoiceWorkStatus.NotFound);
|
||||
|
||||
VoiceWork[] voiceWorks = await dbContext.VoiceWorks
|
||||
.Where(voiceWork => request.VoiceWorkIds.Contains(voiceWork.VoiceWorkId))
|
||||
.Include(x => x.Circle)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
List<VoiceWork> voiceWorksToDelete = [];
|
||||
|
||||
foreach (VoiceWork voiceWork in voiceWorks)
|
||||
{
|
||||
if (results.ContainsKey(voiceWork.VoiceWorkId) == false)
|
||||
{
|
||||
results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.NotFound;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (voiceWork.Circle is null)
|
||||
{
|
||||
results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.NotFound;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (voiceWork.IsValid == true && voiceWork.Circle.Spam == false)
|
||||
{
|
||||
results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.NotAllowed;
|
||||
continue;
|
||||
}
|
||||
|
||||
voiceWorksToDelete.Add(voiceWork);
|
||||
results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.Deleted;
|
||||
}
|
||||
|
||||
int[] voiceWorkIdsToDelete = [.. voiceWorksToDelete.Select(x => x.VoiceWorkId)];
|
||||
|
||||
if (voiceWorkIdsToDelete.Length > 0)
|
||||
{
|
||||
dbContext.VoiceWorks.RemoveRange(voiceWorksToDelete);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await dbContext.VoiceWorkSearches
|
||||
.Where(x => voiceWorkIdsToDelete.Contains(x.VoiceWorkId))
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return new DeleteVoiceWorkResponse(results);
|
||||
}
|
||||
|
||||
private async Task<VoiceWork> GetVoiceWorkAsync(int voiceWorkId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await dbContext.VoiceWorks.FirstOrDefaultAsync(voiceWork => voiceWork.VoiceWorkId == voiceWorkId, cancellationToken)
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="NTextCat" Version="0.3.65" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace JSMR.Infrastructure.Scanning;
|
||||
|
||||
public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksProvider
|
||||
{
|
||||
private const int MaxPeriodDays = 60;
|
||||
|
||||
public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
||||
{
|
||||
DateOnly[] salesDates =
|
||||
@@ -20,20 +22,72 @@ public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksP
|
||||
if (salesDates.Length == 0)
|
||||
return [];
|
||||
|
||||
HashSet<string> productIds = [.. scanResult.Works.Select(x => x.ProductId)];
|
||||
|
||||
DateOnly minDate = salesDates.Min();
|
||||
DateOnly maxDate = salesDates.Max();
|
||||
|
||||
DateOnly requestDate = minDate.AddDays(-1);
|
||||
DateOnly requestEndDate = maxDate.AddDays(1);
|
||||
ReleasedWorksCollection collection = [];
|
||||
|
||||
int period = (requestEndDate.DayNumber - requestDate.DayNumber) + 1;
|
||||
DateOnly chunkStart = minDate;
|
||||
|
||||
ReleasedWorksRequest releasedWorksRequest = new(
|
||||
while (chunkStart <= maxDate)
|
||||
{
|
||||
int endDayNumber = Math.Min(chunkStart.DayNumber + MaxPeriodDays - 1, maxDate.DayNumber);
|
||||
DateOnly chunkEnd = DateOnly.FromDayNumber(endDayNumber);
|
||||
|
||||
int period = chunkEnd.DayNumber - chunkStart.DayNumber + 1;
|
||||
|
||||
ReleasedWorksRequest request = new(
|
||||
Locale: Locale.English,
|
||||
Date: requestEndDate,
|
||||
Period: period
|
||||
);
|
||||
Date: chunkEnd,
|
||||
Period: period);
|
||||
|
||||
return await dlsiteClient.GetReleasedWorksAsync(releasedWorksRequest, cancellationToken);
|
||||
ReleasedWorksCollection chunk = await dlsiteClient.GetReleasedWorksAsync(request, cancellationToken);
|
||||
|
||||
foreach (string productId in chunk.Keys)
|
||||
{
|
||||
if (productIds.Contains(productId) == false)
|
||||
continue;
|
||||
|
||||
if (collection.ContainsKey(productId))
|
||||
continue;
|
||||
|
||||
collection.Add(productId, chunk[productId]);
|
||||
}
|
||||
|
||||
chunkStart = chunkEnd.AddDays(1);
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
//public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
||||
//{
|
||||
// DateOnly[] salesDates =
|
||||
// [
|
||||
// .. scanResult.Works
|
||||
// .Where(x => x.SalesDate.HasValue)
|
||||
// .Select(x => x.SalesDate!.Value)
|
||||
// ];
|
||||
|
||||
// if (salesDates.Length == 0)
|
||||
// return [];
|
||||
|
||||
// DateOnly minDate = salesDates.Min();
|
||||
// DateOnly maxDate = salesDates.Max();
|
||||
|
||||
// DateOnly requestDate = minDate.AddDays(-1);
|
||||
// DateOnly requestEndDate = maxDate.AddDays(1);
|
||||
|
||||
// int period = (requestEndDate.DayNumber - requestDate.DayNumber) + 1;
|
||||
|
||||
// ReleasedWorksRequest releasedWorksRequest = new(
|
||||
// Locale: Locale.English,
|
||||
// Date: requestEndDate,
|
||||
// Period: period
|
||||
// );
|
||||
|
||||
// return await dlsiteClient.GetReleasedWorksAsync(releasedWorksRequest, cancellationToken);
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Infrastructure.Data;
|
||||
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||
using JSMR.Tests.Fixtures;
|
||||
using Shouldly;
|
||||
|
||||
namespace JSMR.Tests.Data.Repositories.VoiceWorks;
|
||||
|
||||
public class Delete_Voice_Work_Tests(MariaDbContainerFixture container) : VoiceWorkRepositoryTests(container)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Try_Delete_Invalid_Voice_Work()
|
||||
{
|
||||
await using AppDbContext dbContext = await GetAppDbContextAsync();
|
||||
VoiceWorkWriter writer = new(dbContext);
|
||||
|
||||
int voiceWorkId = 3;
|
||||
DeleteVoiceWorkRequest request = new([voiceWorkId]);
|
||||
|
||||
dbContext.VoiceWorks.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldNotBeNull();
|
||||
dbContext.VoiceWorkSearches.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldNotBeNull();
|
||||
|
||||
DeleteVoiceWorkResponse response = await writer.DeleteAsync(request, TestContext.Current.CancellationToken);
|
||||
response.Results.Count.ShouldBe(1);
|
||||
response.Results.ShouldContainKey(voiceWorkId);
|
||||
response.Results[voiceWorkId].ShouldBe(DeleteVoiceWorkStatus.Deleted);
|
||||
|
||||
dbContext.VoiceWorks.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldBeNull();
|
||||
dbContext.VoiceWorkSearches.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Try_Delete_Valid_Voice_Work()
|
||||
{
|
||||
await using AppDbContext dbContext = await GetAppDbContextAsync();
|
||||
VoiceWorkWriter writer = new(dbContext);
|
||||
|
||||
int voiceWorkId = 1;
|
||||
DeleteVoiceWorkRequest request = new([voiceWorkId]);
|
||||
|
||||
dbContext.VoiceWorks.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldNotBeNull();
|
||||
dbContext.VoiceWorkSearches.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldNotBeNull();
|
||||
|
||||
DeleteVoiceWorkResponse response = await writer.DeleteAsync(request, TestContext.Current.CancellationToken);
|
||||
response.Results.Count.ShouldBe(1);
|
||||
response.Results.ShouldContainKey(voiceWorkId);
|
||||
response.Results[voiceWorkId].ShouldBe(DeleteVoiceWorkStatus.NotAllowed);
|
||||
|
||||
dbContext.VoiceWorks.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldNotBeNull();
|
||||
dbContext.VoiceWorkSearches.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Try_Delete_Missing_Voice_Work()
|
||||
{
|
||||
await using AppDbContext dbContext = await GetAppDbContextAsync();
|
||||
VoiceWorkWriter writer = new(dbContext);
|
||||
|
||||
int voiceWorkId = 999;
|
||||
DeleteVoiceWorkRequest request = new([voiceWorkId]);
|
||||
|
||||
dbContext.VoiceWorks.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldBeNull();
|
||||
dbContext.VoiceWorkSearches.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldBeNull();
|
||||
|
||||
DeleteVoiceWorkResponse response = await writer.DeleteAsync(request, TestContext.Current.CancellationToken);
|
||||
response.Results.Count.ShouldBe(1);
|
||||
response.Results.ShouldContainKey(voiceWorkId);
|
||||
response.Results[voiceWorkId].ShouldBe(DeleteVoiceWorkStatus.NotFound);
|
||||
|
||||
dbContext.VoiceWorks.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldBeNull();
|
||||
dbContext.VoiceWorkSearches.FirstOrDefault(x => x.VoiceWorkId == voiceWorkId).ShouldBeNull();
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,65 @@ public static class VoiceWorkRepositorySeedData
|
||||
);
|
||||
|
||||
context.VoiceWorks.AddRange(
|
||||
new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 1), Downloads = 500, WishlistCount = 750, StarRating = 35 },
|
||||
new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease, SalesDate = new(2025, 1, 3), Downloads = 5000, WishlistCount = 12000, StarRating = 50, Favorite = true },
|
||||
new() { VoiceWorkId = 3, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 2), Downloads = 50, WishlistCount = 100, StarRating = 20 },
|
||||
new() { VoiceWorkId = 4, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming, ExpectedDate = new(2025, 1, 1), WishlistCount = 300 },
|
||||
new() { VoiceWorkId = 5, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming, ExpectedDate = new(2025, 1, 11), WishlistCount = 10000 }
|
||||
new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 1), Downloads = 500, WishlistCount = 750, StarRating = 35, IsValid = true },
|
||||
new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease, SalesDate = new(2025, 1, 3), Downloads = 5000, WishlistCount = 12000, StarRating = 50, Favorite = true, IsValid = true },
|
||||
new() { VoiceWorkId = 3, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 2), Downloads = 50, WishlistCount = 100, StarRating = 20, IsValid = false },
|
||||
new() { VoiceWorkId = 4, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming, ExpectedDate = new(2025, 1, 1), WishlistCount = 300, IsValid = true },
|
||||
new() { VoiceWorkId = 5, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming, ExpectedDate = new(2025, 1, 11), WishlistCount = 10000, IsValid = true }
|
||||
);
|
||||
|
||||
context.Creators.AddRange(
|
||||
new() { CreatorId = 1, Name = "Average Creator" },
|
||||
new() { CreatorId = 2, Name = "Good Creator" },
|
||||
new() { CreatorId = 3, Name = "Bad Creator" }
|
||||
);
|
||||
|
||||
context.VoiceWorkCreators.AddRange(
|
||||
new() { VoiceWorkId = 1, CreatorId = 1 },
|
||||
new() { VoiceWorkId = 2, CreatorId = 2 },
|
||||
new() { VoiceWorkId = 3, CreatorId = 3 }
|
||||
);
|
||||
|
||||
context.Tags.AddRange(
|
||||
new() { TagId = 1, Name = "ASMR" },
|
||||
new() { TagId = 2, Name = "Tsundere" },
|
||||
new() { TagId = 3, Name = "Office Lady" }
|
||||
);
|
||||
|
||||
context.VoiceWorkTags.AddRange(
|
||||
new() { VoiceWorkId = 1, TagId = 1 },
|
||||
new() { VoiceWorkId = 2, TagId = 2 },
|
||||
new() { VoiceWorkId = 3, TagId = 3 }
|
||||
);
|
||||
|
||||
context.VoiceWorkSupportedLanguages.AddRange(
|
||||
new() { VoiceWorkSupportedLanguageId = 1, VoiceWorkId = 1, Language = "JPN" },
|
||||
new() { VoiceWorkSupportedLanguageId = 2, VoiceWorkId = 2, Language = "JPN" },
|
||||
new() { VoiceWorkSupportedLanguageId = 3, VoiceWorkId = 3, Language = "JPN" }
|
||||
);
|
||||
|
||||
context.EnglishVoiceWorks.AddRange(
|
||||
new() { EnglishVoiceWorkId = 1, VoiceWorkId = 1, ProductName = "Today Sounds", Description = "An average product." },
|
||||
new() { EnglishVoiceWorkId = 2, VoiceWorkId = 2, ProductName = "Super Comfy ASMR", Description = "An amazing product!" },
|
||||
new() { EnglishVoiceWorkId = 3, VoiceWorkId = 3, ProductName = "Low Effort", Description = "A bad product." },
|
||||
new() { EnglishVoiceWorkId = 4, VoiceWorkId = 4, ProductName = "Tomorrow Sounds", Description = "A average upcoming product." },
|
||||
new() { EnglishVoiceWorkId = 5, VoiceWorkId = 5, ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!" }
|
||||
);
|
||||
|
||||
context.VoiceWorkLocalizations.AddRange(
|
||||
new() { VoiceWorkLocalizationId = 1, VoiceWorkId = 1, Language = "JPN", ProductName = "Today Sounds", Description = "An average product." },
|
||||
new() { VoiceWorkLocalizationId = 2, VoiceWorkId = 2, Language = "JPN", ProductName = "Super Comfy ASMR", Description = "An amazing product!" },
|
||||
new() { VoiceWorkLocalizationId = 3, VoiceWorkId = 3, Language = "JPN", ProductName = "Low Effort", Description = "A bad product." },
|
||||
new() { VoiceWorkLocalizationId = 4, VoiceWorkId = 4, Language = "JPN", ProductName = "Tomorrow Sounds", Description = "A average upcoming product." },
|
||||
new() { VoiceWorkLocalizationId = 5, VoiceWorkId = 5, Language = "JPN", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!" }
|
||||
);
|
||||
|
||||
context.VoiceWorkSearches.AddRange(
|
||||
new() { VoiceWorkId = 1, SearchText = "Search 1" },
|
||||
new() { VoiceWorkId = 2, SearchText = "Search 2" },
|
||||
new() { VoiceWorkId = 3, SearchText = "Search 3" },
|
||||
new() { VoiceWorkId = 4, SearchText = "Search 4" },
|
||||
new() { VoiceWorkId = 5, SearchText = "Search 5" }
|
||||
);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
|
||||
288
JSMR.Tests/Unit/ReleasedWorksProviderTests.cs
Normal file
288
JSMR.Tests/Unit/ReleasedWorksProviderTests.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using JSMR.Application.Enums;
|
||||
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||
using JSMR.Application.Integrations.DLSite.Ports;
|
||||
using JSMR.Application.Scanning.Contracts;
|
||||
using JSMR.Infrastructure.Scanning;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JSMR.Tests.Unit;
|
||||
|
||||
public class ReleasedWorksProviderTests
|
||||
{
|
||||
private readonly IDLSiteClient _dlsiteClient = Substitute.For<IDLSiteClient>();
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenNoSalesDates_ReturnsEmptyAndDoesNotCallClient()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = null,
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result = await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
|
||||
await _dlsiteClient.DidNotReceiveWithAnyArgs().GetReleasedWorksAsync(default!, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenRangeIsUnder60Days_CallsClientOnce()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 10),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
},
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
SalesDate = new DateOnly(2024, 1, 20),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
ReleasedWorksCollection apiResult = new()
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "English title 1",
|
||||
Description = "Description",
|
||||
MaskedTitle = "English title 1",
|
||||
MaskedDescription = "Description",
|
||||
},
|
||||
["RJ002"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
Title = "English title 2",
|
||||
Description = "Description",
|
||||
MaskedTitle = "English title 2",
|
||||
MaskedDescription = "Description",
|
||||
}
|
||||
};
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(Arg.Any<ReleasedWorksRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(apiResult);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result =
|
||||
await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result.Keys.ShouldBe(["RJ001", "RJ002"], ignoreOrder: true);
|
||||
|
||||
await _dlsiteClient.Received(1).GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x =>
|
||||
x.Locale == Locale.English &&
|
||||
x.Date == new DateOnly(2024, 1, 20) &&
|
||||
x.Period == 11),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenRangeExceeds60Days_SplitsIntoMultipleRequests()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 1),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
},
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
SalesDate = new DateOnly(2024, 3, 5),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(Arg.Any<ReleasedWorksRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
await _dlsiteClient.Received(1).GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x =>
|
||||
x.Date == new DateOnly(2024, 2, 29) &&
|
||||
x.Period == 60),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _dlsiteClient.Received(1).GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x =>
|
||||
x.Date == new DateOnly(2024, 3, 5) &&
|
||||
x.Period == 5),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _dlsiteClient.Received(2).GetReleasedWorksAsync(
|
||||
Arg.Any<ReleasedWorksRequest>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_FiltersOutProductsNotInScanResult()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 10),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
ReleasedWorksCollection apiResult = new()
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "Keep me",
|
||||
Description = "Description",
|
||||
MaskedTitle = "Keep me",
|
||||
MaskedDescription = "Description",
|
||||
},
|
||||
["RJ999"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ999",
|
||||
Title = "Ignore me",
|
||||
Description = "Description",
|
||||
MaskedTitle = "Ignore me",
|
||||
MaskedDescription = "Description",
|
||||
},
|
||||
};
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(Arg.Any<ReleasedWorksRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(apiResult);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result =
|
||||
await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result.Keys.ShouldBe(["RJ001"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenSameProductReturnedTwice_KeepsFirstResult()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 1),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
},
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
SalesDate = new DateOnly(2024, 3, 5),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x => x.Period == 60),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(new ReleasedWorksCollection
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "First",
|
||||
Description = "Description",
|
||||
MaskedTitle = "First",
|
||||
MaskedDescription = "Description",
|
||||
}
|
||||
});
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x => x.Period == 5),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(new ReleasedWorksCollection
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "Second",
|
||||
Description = "Description",
|
||||
MaskedTitle = "Second",
|
||||
MaskedDescription = "Description",
|
||||
}
|
||||
});
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result = await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result["RJ001"].Title.ShouldBe("First");
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,10 @@
|
||||
UseCurrentColor>
|
||||
</Icon>
|
||||
}
|
||||
@if (ChildContent is not null)
|
||||
{
|
||||
<span>@ChildContent</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -26,7 +29,10 @@ else
|
||||
UseCurrentColor>
|
||||
</Icon>
|
||||
}
|
||||
@if (ChildContent is not null)
|
||||
{
|
||||
<span>@ChildContent</span>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -62,6 +68,12 @@ else
|
||||
[Parameter]
|
||||
public EventCallback Click { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsClickable { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool ThickBorder { get; set; }
|
||||
|
||||
private string GetClasses()
|
||||
{
|
||||
string color = Color.ToString().ToLower();
|
||||
@@ -72,6 +84,17 @@ else
|
||||
$"color-{color}"
|
||||
];
|
||||
|
||||
// Experimental
|
||||
if (ChildContent is null)
|
||||
{
|
||||
classNames.Add("j-chip-icon-only");
|
||||
}
|
||||
|
||||
if (ThickBorder)
|
||||
{
|
||||
classNames.Add("j-chip-thick-border");
|
||||
}
|
||||
|
||||
switch (Varient)
|
||||
{
|
||||
case ElementVarient.Filled:
|
||||
@@ -99,7 +122,7 @@ else
|
||||
classNames.Add($"varient-tint");
|
||||
}
|
||||
|
||||
if (Click.HasDelegate || string.IsNullOrWhiteSpace(Url) == false)
|
||||
if (Click.HasDelegate || string.IsNullOrWhiteSpace(Url) == false || IsClickable)
|
||||
{
|
||||
classNames.Add("is-clickable");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
@using JSMR.Application.VoiceWorks.Queries.Search
|
||||
@using AntDesign
|
||||
@using JSMR.Application.VoiceWorks.Commands.Delete
|
||||
@using JSMR.Application.VoiceWorks.Commands.SetFavorite
|
||||
@using JSMR.Application.VoiceWorks.Queries.Search
|
||||
@using JSMR.Domain.Enums
|
||||
@using JSMR.UI.Blazor.Components.Chips
|
||||
@using JSMR.UI.Blazor.Enums
|
||||
@@ -7,6 +10,10 @@
|
||||
@using System.Globalization
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
|
||||
@inject VoiceWorksClient Client
|
||||
@inject MessageService MessageService
|
||||
@inject ModalService ModalService
|
||||
|
||||
<div class=@GetCardClasses(Product)>
|
||||
<div class="j-voice-work-image-container">
|
||||
<JImage OverlayClass="j-voice-work-image-overlay" ImageClass="j-voice-work-image" Source="@ImageUrlProvider.GetImageUrl(Product, "main")" FallbackSource="@ImageUrlProvider.GetImageUrl(Product, "main", "webp")"></JImage>
|
||||
@@ -16,12 +23,6 @@
|
||||
<a href="@Product.ProductUrl" target="_blank">@Product.ProductName</a>
|
||||
</div>
|
||||
<BitStack Horizontal="true" Gap="0.5em" AutoHeight Wrap>
|
||||
@* <MudChip T="string"
|
||||
Href=@($"https://www.dlsite.com/maniax/circle/profile/=/maker_id/{Product.MakerId}.html")
|
||||
Target="_blank"
|
||||
Variant="MudBlazor.Variant.Filled"
|
||||
Icon="@Icons.Material.Outlined.Circle">@Product.Maker</MudChip> *@
|
||||
@* <CircleChip Circle="@Product.Circle"></CircleChip> *@
|
||||
<Chip Graphic="Graphic.Circle" Varient="ElementVarient.Outlined" Color="ColorVarient.Secondary" Url=@($"https://www.dlsite.com/maniax/circle/profile/=/maker_id/{Product.MakerId}.html")>@Product.Maker</Chip>
|
||||
|
||||
@if (Product.OriginalCircle is not null)
|
||||
@@ -31,12 +32,6 @@
|
||||
|
||||
@foreach (var creator in Product.Creators)
|
||||
{
|
||||
@* <MudChip T="string"
|
||||
Href=@($"https://www.dlsite.com/maniax/fsr/=/keyword_creater/{creator.Name}")
|
||||
Target="_blank"
|
||||
Variant="MudBlazor.Variant.Filled"
|
||||
Icon="@Icons.Material.Filled.Person">@creator.Name</MudChip> *@
|
||||
@* <CreatorChip Creator="@creator"></CreatorChip> *@
|
||||
<Chip Graphic="Graphic.Person" Varient="ElementVarient.Outlined" IconVarient="IconVarient.Fill" Color="ColorVarient.Secondary" Url=@($"https://www.dlsite.com/maniax/fsr/=/keyword_creater/{creator.Name}")>@creator.Name</Chip>
|
||||
}
|
||||
</BitStack>
|
||||
@@ -70,16 +65,9 @@
|
||||
<div class="j-tags">
|
||||
@foreach (var tag in Product.Tags)
|
||||
{
|
||||
@* <div class="j-tag">@tag.Name</div> *@
|
||||
<ProductTag Tag="tag"></ProductTag>
|
||||
}
|
||||
</div>
|
||||
@* <div class="j-tags">
|
||||
@foreach (var tag in Product.Tags)
|
||||
{
|
||||
<TagChip Tag="tag"></TagChip>
|
||||
}
|
||||
</div> *@
|
||||
</div>
|
||||
<div class="j-voice-work-info">
|
||||
<div class="j-release-date-container">
|
||||
@@ -102,20 +90,45 @@
|
||||
<BitStack Horizontal="true" Gap="0.5rem" VerticalAlign="BitAlignment.End" HorizontalAlign="BitAlignment.End">
|
||||
@if (Product.IsValid != true)
|
||||
{
|
||||
<ProductIndicator Graphic="Graphic.Warning" IconVarient="IconVarient.Fill" Color="ColorVarient.Orange" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
@* <ProductIndicator Graphic="Graphic.Warning" IconVarient="IconVarient.Fill" Color="ColorVarient.Orange" BackgroundColor="ColorVarient.Black"></ProductIndicator> *@
|
||||
<Chip Graphic="Graphic.Warning" IconVarient="IconVarient.Fill" Varient="ElementVarient.Outlined" Color="ColorVarient.Orange" ThickBorder></Chip>
|
||||
}
|
||||
@if (Product.OriginalProductId is not null)
|
||||
{
|
||||
<ProductIndicator Graphic="Graphic.Translate" Color="ColorVarient.Primary" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
@* <ProductIndicator Graphic="Graphic.Translate" Color="ColorVarient.Primary" BackgroundColor="ColorVarient.Black"></ProductIndicator> *@
|
||||
<Chip Graphic="Graphic.Translate" Varient="ElementVarient.Outlined" Color="ColorVarient.Primary" ThickBorder></Chip>
|
||||
}
|
||||
@if (Product.Favorite)
|
||||
{
|
||||
<ProductIndicator Graphic="Graphic.Star" Color="ColorVarient.Pink" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
@* <ProductIndicator Graphic="Graphic.Star" Color="ColorVarient.Pink" BackgroundColor="ColorVarient.Black"></ProductIndicator> *@
|
||||
<Chip Graphic="Graphic.Star" Varient="ElementVarient.Outlined" Color="ColorVarient.Pink" ThickBorder></Chip>
|
||||
}
|
||||
@if (Product.HasTrial || Product.HasChobit)
|
||||
{
|
||||
<ProductIndicator Graphic="Graphic.Headphones" Color="ColorVarient.Blue" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
@* <ProductIndicator Graphic="Graphic.Headphones" Color="ColorVarient.Blue" BackgroundColor="ColorVarient.Black"></ProductIndicator> *@
|
||||
<Chip Graphic="Graphic.Headphones" Varient="ElementVarient.Outlined" Color="ColorVarient.Blue" ThickBorder></Chip>
|
||||
}
|
||||
<Dropdown Trigger="@([Trigger.Click])">
|
||||
<Overlay>
|
||||
<Menu Selectable="false">
|
||||
@if (!Product.Favorite)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetFavorite(true)">Add to Favorites</MenuItem>
|
||||
}
|
||||
@if (Product.Favorite)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetFavorite(false)">Remove from Favorites</MenuItem>
|
||||
}
|
||||
@if (Product.IsValid != true)
|
||||
{
|
||||
<MenuItem OnClick="(e) => Delete()">Delete</MenuItem>
|
||||
}
|
||||
</Menu>
|
||||
</Overlay>
|
||||
<ChildContent>
|
||||
<Chip Graphic="Graphic.Pencil" Varient="ElementVarient.Outlined" Color="ColorVarient.Surface" IsClickable ThickBorder></Chip>
|
||||
</ChildContent>
|
||||
</Dropdown>
|
||||
</BitStack>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,6 +137,9 @@
|
||||
[Parameter]
|
||||
public required VoiceWorkSearchResult Product { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback ProductDeleted { get; set; }
|
||||
|
||||
private string GetCardClasses(VoiceWorkSearchResult voiceWork)
|
||||
{
|
||||
List<string> classNames = ["j-card", "j-voice-work-card"];
|
||||
@@ -194,4 +210,74 @@
|
||||
default: return code;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetFavorite(bool value)
|
||||
{
|
||||
SetVoiceWorkFavoriteRequest request = new(
|
||||
VoiceWorkId: Product.VoiceWorkId,
|
||||
IsFavorite: value
|
||||
);
|
||||
|
||||
SetVoiceWorkFavoriteResponse? response = await Client.SetVoiceWorkFavoriteeAsync(request);
|
||||
|
||||
if (response is not null)
|
||||
{
|
||||
Product.Favorite = response.IsFavorite;
|
||||
}
|
||||
|
||||
//await InvokeAsync(StateHasChanged);
|
||||
|
||||
MessageConfig messageConfig = new()
|
||||
{
|
||||
Content = $"Product '{Product.ProductName}' has been {(Product.Favorite ? "added to your favorites" : "removed from your favorites")}.",
|
||||
Type = MessageType.Success
|
||||
};
|
||||
|
||||
_ = MessageService.OpenAsync(messageConfig);
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
RenderFragment icon = @<AntDesign.Icon Type="@IconType.Outline.ExclamationCircle" />;
|
||||
|
||||
AntDesign.ConfirmOptions options = new()
|
||||
{
|
||||
Title = "Are you sure you want to delete the following product?",
|
||||
Icon = icon,
|
||||
Content = Product.ProductName,
|
||||
Centered = true,
|
||||
OnOk = async (e) =>
|
||||
{
|
||||
DeleteVoiceWorkRequest request = new(
|
||||
VoiceWorkIds: [Product.VoiceWorkId]
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
DeleteVoiceWorkResponse? response = await Client.DeleteVoiceWorkAsync(request);
|
||||
|
||||
if (response is null || response.Results[Product.VoiceWorkId] != DeleteVoiceWorkStatus.Deleted)
|
||||
return;
|
||||
|
||||
await ProductDeleted.InvokeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AntDesign.ConfirmOptions errorOptions = new()
|
||||
{
|
||||
Title = "Unable to delete product",
|
||||
Content = "Something went wrong while deleting this product. The product was not deleted. Check the API logs for details.",
|
||||
Centered = true,
|
||||
//Width = "70vw",
|
||||
};
|
||||
|
||||
await ModalService.ErrorAsync(errorOptions);
|
||||
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await ModalService.ConfirmAsync(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ else
|
||||
<div class="j-product-items-container">
|
||||
@foreach (var product in Products)
|
||||
{
|
||||
<JProduct Product="@product"></JProduct>
|
||||
<JProduct Product="@product" ProductDeleted="OnProductDeleted"></JProduct>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -21,4 +21,12 @@ else
|
||||
@code {
|
||||
[Parameter]
|
||||
public VoiceWorkSearchResult[]? Products { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback ProductDeleted { get; set; }
|
||||
|
||||
private async Task OnProductDeleted()
|
||||
{
|
||||
await ProductDeleted.InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
19
JSMR.UI.Blazor/Components/Modals/TagEnglishNameModal.razor
Normal file
19
JSMR.UI.Blazor/Components/Modals/TagEnglishNameModal.razor
Normal file
@@ -0,0 +1,19 @@
|
||||
@inherits FeedbackComponent<SetEnglishTagNameModel>
|
||||
|
||||
@using AntDesign
|
||||
@using JSMR.UI.Blazor.Models
|
||||
|
||||
<Form Model="@Options">
|
||||
<Input @bind-Value="@Options.UpdatedEnglishTagName" Placeholder="English Name" AutoFocus />
|
||||
</Form>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
public ModalRef ModalRef { get; set; } = default!;
|
||||
|
||||
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
|
||||
{
|
||||
await base.OnFeedbackOkAsync(args);
|
||||
await CloseFeedbackAsync();
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,6 @@ public enum Graphic
|
||||
Age,
|
||||
Calendar,
|
||||
Download,
|
||||
Microphone
|
||||
Microphone,
|
||||
Pencil
|
||||
}
|
||||
9
JSMR.UI.Blazor/Exceptions/ApiException.cs
Normal file
9
JSMR.UI.Blazor/Exceptions/ApiException.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Net;
|
||||
|
||||
namespace JSMR.UI.Blazor.Exceptions;
|
||||
|
||||
public sealed class ApiException(HttpStatusCode statusCode, string message, string? responseBody = null) : Exception(message)
|
||||
{
|
||||
public HttpStatusCode StatusCode { get; } = statusCode;
|
||||
public string? ResponseBody { get; } = responseBody;
|
||||
}
|
||||
109
JSMR.UI.Blazor/Filters/TagFilterState.cs
Normal file
109
JSMR.UI.Blazor/Filters/TagFilterState.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using JSMR.Application.Common.Search;
|
||||
using JSMR.Application.Tags.Queries.Search;
|
||||
using JSMR.Application.Tags.Queries.Search.Contracts;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Globalization;
|
||||
|
||||
namespace JSMR.UI.Blazor.Filters;
|
||||
|
||||
public sealed record TagFilterState : IFilterState<SearchTagsRequest>
|
||||
{
|
||||
public string? Keywords { get; init; }
|
||||
public int PageNumber { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 100;
|
||||
|
||||
public IReadOnlyList<SortOption<TagSortField>> SortOptions { get; init; } =
|
||||
[
|
||||
new(TagSortField.Name, SortDirection.Ascending)
|
||||
];
|
||||
|
||||
public QueryParameters ToQuery()
|
||||
{
|
||||
QueryParameters query = [];
|
||||
|
||||
query.Set("keywords", Keywords);
|
||||
|
||||
if (PageNumber != 1)
|
||||
query.Set("pageNumber", PageNumber.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
if (PageSize != 100)
|
||||
query.Set("pageSize", PageSize.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
if (!IsDefaultSort(SortOptions))
|
||||
{
|
||||
query.Set("sort", string.Join(",", SortOptions.Select(x =>
|
||||
$"{x.Field}:{ToQueryDirection(x.Direction)}")));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public static TagFilterState FromQuery(string query)
|
||||
{
|
||||
QueryParameters queryParameters = new(QueryHelpers.ParseQuery(query));
|
||||
|
||||
return new TagFilterState
|
||||
{
|
||||
Keywords = queryParameters.GetValue("keywords"),
|
||||
PageNumber = Math.Max(1, queryParameters.GetInteger("pageNumber", 1)),
|
||||
PageSize = queryParameters.GetInteger("pageSize", 100),
|
||||
SortOptions = ParseSortOptions(queryParameters.GetValue("sort"))
|
||||
};
|
||||
}
|
||||
|
||||
public SearchTagsRequest ToSearchRequest()
|
||||
{
|
||||
return new(
|
||||
Options: new()
|
||||
{
|
||||
PageNumber = PageNumber,
|
||||
PageSize = PageSize,
|
||||
Criteria = new()
|
||||
{
|
||||
Name = Keywords
|
||||
},
|
||||
SortOptions = [.. SortOptions]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SortOption<TagSortField>> ParseSortOptions(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return DefaultSort();
|
||||
|
||||
var result = new List<SortOption<TagSortField>>();
|
||||
|
||||
foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var pieces = part.Split(':', 2, StringSplitOptions.TrimEntries);
|
||||
|
||||
if (pieces.Length != 2)
|
||||
continue;
|
||||
|
||||
if (!Enum.TryParse<TagSortField>(pieces[0], ignoreCase: true, out var field))
|
||||
continue;
|
||||
|
||||
var direction = pieces[1].Equals("desc", StringComparison.OrdinalIgnoreCase)
|
||||
? Application.Common.Search.SortDirection.Descending
|
||||
: Application.Common.Search.SortDirection.Ascending;
|
||||
|
||||
result.Add(new(field, direction));
|
||||
}
|
||||
|
||||
return result.Count > 0 ? result : DefaultSort();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SortOption<TagSortField>> DefaultSort() =>
|
||||
[
|
||||
new(TagSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
||||
];
|
||||
|
||||
private static string ToQueryDirection(Application.Common.Search.SortDirection direction)
|
||||
=> direction == Application.Common.Search.SortDirection.Descending ? "desc" : "asc";
|
||||
|
||||
private static bool IsDefaultSort(IReadOnlyList<SortOption<TagSortField>> sortOptions)
|
||||
=> sortOptions.Count == 1
|
||||
&& sortOptions[0].Field == TagSortField.Name
|
||||
&& sortOptions[0].Direction == Application.Common.Search.SortDirection.Ascending;
|
||||
}
|
||||
@@ -12,13 +12,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AntDesign" Version="1.6.1" />
|
||||
<PackageReference Include="Bit.BlazorUI" Version="10.4.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.7" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.7" />
|
||||
<PackageReference Include="MudBlazor" Version="9.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.8" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
|
||||
<PackageReference Include="MudBlazor" Version="9.4.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Radzen.Blazor" Version="10.3.0" />
|
||||
<PackageReference Include="Radzen.Blazor" Version="10.4.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using JSMR.UI.Blazor.Components
|
||||
@using AntDesign
|
||||
@using JSMR.UI.Blazor.Components
|
||||
@using JSMR.UI.Blazor.Services
|
||||
|
||||
@inject SessionState Session
|
||||
@@ -6,6 +7,8 @@
|
||||
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<AntContainer />
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="1" Dense="@_dense">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="MudBlazor.Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
|
||||
@@ -40,6 +43,7 @@
|
||||
<MudMainContent Class="pt-18 px-8">
|
||||
@Body
|
||||
</MudMainContent>
|
||||
|
||||
@* Required *@
|
||||
<MudThemeProvider @ref="_mudThemeProvider" @bind-IsDarkMode="_isDarkMode" />
|
||||
<MudPopoverProvider />
|
||||
@@ -49,10 +53,10 @@
|
||||
|
||||
@* Needed for snackbars *@
|
||||
<MudSnackbarProvider />
|
||||
|
||||
</MudLayout>
|
||||
|
||||
<RadzenComponents @rendermode="RenderMode.InteractiveAuto" />
|
||||
<AntContainer />
|
||||
|
||||
@code{
|
||||
private bool _open = false;
|
||||
|
||||
8
JSMR.UI.Blazor/Models/SetEnglishTagNameModel.cs
Normal file
8
JSMR.UI.Blazor/Models/SetEnglishTagNameModel.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace JSMR.UI.Blazor.Models;
|
||||
|
||||
public class SetEnglishTagNameModel
|
||||
{
|
||||
public required string TagName { get; set; }
|
||||
public string? CurrentEnglishTagName { get; set; }
|
||||
public string? UpdatedEnglishTagName { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
@page "/creators"
|
||||
@inject VoiceWorksClient Client
|
||||
@inject IJSRuntime JS
|
||||
@using AntDesign
|
||||
@using JSMR.Application.Common.Search
|
||||
@using JSMR.Application.Creators.Commands.UpdateCreatorStatus
|
||||
@using JSMR.Application.Creators.Contracts
|
||||
@using JSMR.Application.Creators.Queries.Search
|
||||
@using JSMR.Application.Creators.Queries.Search.Contracts
|
||||
@using JSMR.UI.Blazor.Components
|
||||
@@ -50,20 +53,29 @@
|
||||
<AntDesign.Tag Color="AntDesign.TagColor.RedInverse">Blacklisted</AntDesign.Tag>
|
||||
}
|
||||
</AntDesign.PropertyColumn>
|
||||
@* <AntDesign.ActionColumn HeaderStyle="width: 2em">
|
||||
<AntDesign.Dropdown Trigger="@(new AntDesign.Trigger[] { AntDesign.Trigger.Click })">
|
||||
<AntDesign.ActionColumn HeaderStyle="width: 5em;" Style="text-align: center">
|
||||
<Dropdown Trigger="@([Trigger.Click])">
|
||||
<Overlay>
|
||||
<AntDesign.Menu>
|
||||
<AntDesign.MenuItem>
|
||||
<span>Some Menu Item</span>
|
||||
</AntDesign.MenuItem>
|
||||
</AntDesign.Menu>
|
||||
<Menu Selectable="false">
|
||||
@if (!context.Favorite)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetStatus(context, CreatorStatus.Favorite)">Set as Favorite</MenuItem>
|
||||
}
|
||||
@if (!context.Blacklisted)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetStatus(context, CreatorStatus.Blacklisted)">Set as Blacklisted</MenuItem>
|
||||
}
|
||||
@if (context.Favorite || context.Blacklisted)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetStatus(context, CreatorStatus.Neutral)">Set as Neutral</MenuItem>
|
||||
}
|
||||
</Menu>
|
||||
</Overlay>
|
||||
<ChildContent>
|
||||
<AntDesign.Button Shape="AntDesign.ButtonShape.Circle" Icon="@AntDesign.IconType.Outline.More" Type="AntDesign.ButtonType.Default"></AntDesign.Button>
|
||||
<Button Icon="Ellipsis"></Button>
|
||||
</ChildContent>
|
||||
</AntDesign.Dropdown>
|
||||
</AntDesign.ActionColumn> *@
|
||||
</Dropdown>
|
||||
</AntDesign.ActionColumn>
|
||||
</ColumnDefinitions>
|
||||
</AntDesign.Table>
|
||||
<JPagination2 PageNumber="PageNumber" PageNumberChanged="OnPageNumberChanged" PageSize="PageSize" PageSizeChanged="OnPageSizeChanged" TotalItems="TotalItems" />
|
||||
@@ -119,6 +131,9 @@
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
INotificationService NotificationService { get; set; } = default!;
|
||||
|
||||
public string? Keywords { get; set; }
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 100;
|
||||
@@ -259,4 +274,31 @@
|
||||
|
||||
NavigationManager.NavigateTo(uri);
|
||||
}
|
||||
|
||||
private async Task SetStatus(CreatorSearchItem item, CreatorStatus status)
|
||||
{
|
||||
UpdateCreatorStatusRequest request = new(
|
||||
CreatorId: item.CreatorId,
|
||||
CreatorStatus: status
|
||||
);
|
||||
|
||||
UpdateCreatorStatusResponse? response = await Client.UpdateCreatorStatusAsync(request);
|
||||
|
||||
if (response is not null)
|
||||
{
|
||||
item.Favorite = response.CreatorStatus is CreatorStatus.Favorite;
|
||||
item.Blacklisted = response.CreatorStatus is CreatorStatus.Blacklisted;
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var config = new NotificationConfig()
|
||||
{
|
||||
Message = $"Creator Status Update",
|
||||
Description = $"Creator '{item.Name}' set to {status.ToString()}.",
|
||||
Placement = NotificationPlacement.Top
|
||||
};
|
||||
|
||||
await NotificationService.Open(config);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
<BitPivot Size="BitSize.Medium">
|
||||
<BitPivotItem HeaderText="@($"Available ({availableVoiceWorks?.Length ?? 0})")">
|
||||
<JProductCollection Products="availableVoiceWorks"></JProductCollection>
|
||||
<JProductCollection Products="availableVoiceWorks" ProductDeleted="OnAvailableProductDeleted"></JProductCollection>
|
||||
</BitPivotItem>
|
||||
<BitPivotItem HeaderText="@($"Upcoming ({upcomingVoiceWorks?.Length ?? 0})")">
|
||||
<JProductCollection Products="upcomingVoiceWorks"></JProductCollection>
|
||||
<JProductCollection Products="upcomingVoiceWorks" ProductDeleted="OnUpcomingProductDeleted"></JProductCollection>
|
||||
</BitPivotItem>
|
||||
<BitPivotItem HeaderText="@($"Announcements ({announcedVoiceWorks?.Length ?? 0})")">
|
||||
<JProductCollection Products="announcedVoiceWorks"></JProductCollection>
|
||||
<JProductCollection Products="announcedVoiceWorks" ProductDeleted="OnAnnouncedProductDeleted"></JProductCollection>
|
||||
</BitPivotItem>
|
||||
</BitPivot>
|
||||
|
||||
@@ -105,4 +105,21 @@
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnAvailableProductDeleted()
|
||||
{
|
||||
_ = LoadAvailableVoiceWorksAsync();
|
||||
}
|
||||
|
||||
private async Task OnUpcomingProductDeleted()
|
||||
{
|
||||
_ = LoadUpcomingVoiceWorksAsync();
|
||||
_ = LoadAnnouncedVoiceWorksAsync();
|
||||
}
|
||||
|
||||
private async Task OnAnnouncedProductDeleted()
|
||||
{
|
||||
_ = LoadUpcomingVoiceWorksAsync();
|
||||
_ = LoadAnnouncedVoiceWorksAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
@page "/login"
|
||||
@layout LoginLayout
|
||||
|
||||
@using AntDesign
|
||||
@using JSMR.UI.Blazor.Services
|
||||
@using MudBlazor.Charts
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
|
||||
@inject SessionState Session
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<h3>Login</h3>
|
||||
@* <h3>Login</h3> *@
|
||||
|
||||
<PageTitle>Sign In - JSMR</PageTitle>
|
||||
|
||||
@if (Session.IsAuthenticated)
|
||||
{
|
||||
@@ -15,27 +20,83 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="max-width: 360px;">
|
||||
<BitCard>
|
||||
<BitStack>
|
||||
<BitTextField Label="Username" @bind-Value="username"></BitTextField>
|
||||
<BitTextField Label="Password" @bind-Value="password" Type="BitInputType.Password"></BitTextField>
|
||||
<BitButton OnClick="LoginAsync" IsEnabled="@(!busy)">Login</BitButton>
|
||||
<div class="login-container">
|
||||
<AntDesign.Card Title=@("Sign In") Class="ant-blurred-card">
|
||||
<Body>
|
||||
<AntDesign.Form Model="@loginModel" Layout="FormLayout.Vertical">
|
||||
<AntDesign.FormItem Label="Username">
|
||||
<AntDesign.Input @bind-Value="context.Username"></AntDesign.Input>
|
||||
</AntDesign.FormItem>
|
||||
<AntDesign.FormItem Label="Password">
|
||||
<AntDesign.InputPassword @bind-Value="context.Password"></AntDesign.InputPassword>
|
||||
</AntDesign.FormItem>
|
||||
<AntDesign.Button Class="login-button" OnClick="Login2Async" Disabled="@(busy)" Type="AntDesign.ButtonType.Primary">Sign In</AntDesign.Button>
|
||||
</AntDesign.Form>
|
||||
</Body>
|
||||
</AntDesign.Card>
|
||||
@if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
<p style="color: crimson; margin-top: 8px;">@error</p>
|
||||
<Alert Type="AlertType.Error" Message="@error" ShowIcon="false" />
|
||||
}
|
||||
</BitStack>
|
||||
</BitCard>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
body {
|
||||
/* background-image: url(https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ETkNSJ-oUGwAAAAAQ_AAAAgAegCCAQ/original);
|
||||
background-size: cover; */
|
||||
background: #1b1f23;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-form-item-required {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: .25rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
max-width: 450px;
|
||||
padding: 1rem;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-blurred-card {
|
||||
background-color: color-mix(in srgb, #141414 70%, transparent);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private string username = "";
|
||||
private string password = "";
|
||||
private bool busy;
|
||||
private string? error;
|
||||
|
||||
private LoginModel loginModel = new();
|
||||
|
||||
public class LoginModel
|
||||
{
|
||||
[Required]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
busy = true;
|
||||
@@ -61,6 +122,43 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Login2Async()
|
||||
{
|
||||
busy = true;
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(loginModel.Username))
|
||||
{
|
||||
error = "Username is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(loginModel.Password))
|
||||
{
|
||||
error = "Password is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
var ok = await Session.LoginAsync(loginModel.Username, loginModel.Password);
|
||||
if (!ok)
|
||||
{
|
||||
error = "Invalid username or password.";
|
||||
return;
|
||||
}
|
||||
|
||||
Nav.NavigateTo("/");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Logout()
|
||||
{
|
||||
busy = true;
|
||||
|
||||
@@ -3,23 +3,28 @@
|
||||
@inject IJSRuntime JS
|
||||
@using AntDesign
|
||||
@using JSMR.Application.Common.Search
|
||||
@using JSMR.Application.Tags.Commands.SetEnglishName
|
||||
@using JSMR.Application.Tags.Commands.UpdateTagStatus
|
||||
@using JSMR.Application.Tags.Contracts
|
||||
@using JSMR.Application.Tags.Queries.Search
|
||||
@using JSMR.Application.Tags.Queries.Search.Contracts
|
||||
@using JSMR.UI.Blazor.Components
|
||||
@using JSMR.UI.Blazor.Components.Modals
|
||||
@using JSMR.UI.Blazor.Filters
|
||||
@using JSMR.UI.Blazor.Models
|
||||
@using JSMR.UI.Blazor.Services
|
||||
@using JSMR.UI.Blazor.Shared
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
@using System.Text.Json
|
||||
|
||||
@inherits SearchPageBase<TagFilterState, TagSearchItem>
|
||||
|
||||
<PageTitle>Tags</PageTitle>
|
||||
|
||||
<div class="fdsfds">
|
||||
@* <h1>Tags</h1> *@
|
||||
|
||||
@* <MudTextField T="string" Value="Keywords" ValueChanged="OnKeywordsChanged" Immediate="true" DebounceInterval="500" Label="Filter" Variant="MudBlazor.Variant.Text" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" />
|
||||
*@
|
||||
<AntDesign.Card Title=@("Tags") Class="ant-blurred-card">
|
||||
<Extra>
|
||||
<AntDesign.Input TValue="string" Value="Keywords" ValueChanged="OnKeywordsChanged" DebounceMilliseconds="500" Placeholder="Filter">
|
||||
<AntDesign.Input TValue="string" Value="State.Keywords" ValueChanged="OnKeywordsChanged" DebounceMilliseconds="500" Placeholder="Filter">
|
||||
<Prefix>
|
||||
<AntDesign.Icon Type="@AntDesign.IconType.Outline.Search" />
|
||||
</Prefix>
|
||||
@@ -27,10 +32,10 @@
|
||||
</Extra>
|
||||
<Body>
|
||||
<AntDesign.Table Responsive
|
||||
DataSource="@(searchResults?.Items ?? Enumerable.Empty<TagSearchItem>())"
|
||||
Total="@(searchResults?.TotalItems ?? 0)"
|
||||
DataSource="@(Result?.Items ?? Enumerable.Empty<TagSearchItem>())"
|
||||
Total="@(Result?.TotalItems ?? 0)"
|
||||
TItem="TagSearchItem"
|
||||
Loading="LoadingData"
|
||||
Loading="IsLoading"
|
||||
HidePagination="@true"
|
||||
RemoteDataSource="@true"
|
||||
RowKey="x=>x.TagId"
|
||||
@@ -53,14 +58,41 @@
|
||||
<AntDesign.Tag Color="AntDesign.TagColor.RedInverse">Blacklisted</AntDesign.Tag>
|
||||
}
|
||||
</AntDesign.PropertyColumn>
|
||||
<AntDesign.ActionColumn HeaderStyle="width: 5em;" Style="text-align: center">
|
||||
<Dropdown Trigger="@([Trigger.Click])">
|
||||
<Overlay>
|
||||
<Menu Selectable="false">
|
||||
<SubMenu Title="Set Status" Placement="@AntDesign.Placement.LeftTop">
|
||||
@if (!context.Favorite)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetStatus(context, TagStatus.Favorite)">Favorite</MenuItem>
|
||||
}
|
||||
@if (!context.Blacklisted)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetStatus(context, TagStatus.Blacklisted)">Blacklisted</MenuItem>
|
||||
}
|
||||
@if (context.Favorite || context.Blacklisted)
|
||||
{
|
||||
<MenuItem OnClick="(e) => SetStatus(context, TagStatus.Neutral)">Neutral</MenuItem>
|
||||
}
|
||||
</SubMenu>
|
||||
<MenuItem OnClick="(e) => OpenSetEnglishNameModal(context)">Set English Name...</MenuItem>
|
||||
</Menu>
|
||||
</Overlay>
|
||||
<ChildContent>
|
||||
<Button Icon="Ellipsis"></Button>
|
||||
</ChildContent>
|
||||
</Dropdown>
|
||||
</AntDesign.ActionColumn>
|
||||
</ColumnDefinitions>
|
||||
</AntDesign.Table>
|
||||
<JPagination2 PageNumber="PageNumber" PageNumberChanged="OnPageNumberChanged" PageSize="PageSize" PageSizeChanged="OnPageSizeChanged" TotalItems="TotalItems" />
|
||||
<JPagination2 PageNumber="State.PageNumber"
|
||||
PageNumberChanged="@(pageNumber => UpdateAsync(State with { PageNumber = pageNumber }))"
|
||||
PageSize="State.PageSize"
|
||||
PageSizeChanged="@(pageSize => UpdateAsync(State with { PageSize = pageSize, PageNumber = 1 }))"
|
||||
TotalItems="@(Result?.TotalItems ?? 0)" />
|
||||
</Body>
|
||||
</AntDesign.Card>
|
||||
|
||||
@* <Pagination Current="PageNumber" OnChange="OnPaginationChange" PageSize="PageSize" Total="TotalItems" ShowTotal="ShowTotal"></Pagination>
|
||||
<JPagination PageNumber="PageNumber" PageNumberChanged="OnPageNumberChanged" PageSize="PageSize" PageSizeChanged="OnPageSizeChanged" TotalItems="TotalItems" /> *@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -124,98 +156,39 @@
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
public string? Keywords { get; set; }
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 100;
|
||||
public int TotalItems => searchResults?.TotalItems ?? 0;
|
||||
[Inject]
|
||||
INotificationService NotificationService { get; set; } = default!;
|
||||
|
||||
Func<PaginationTotalContext, string> ShowTotal = ctx => $"{ctx.Range.from} - {ctx.Range.to} of {ctx.Total} items";
|
||||
[Inject]
|
||||
IMessageService MessageService { get; set; } = default!;
|
||||
|
||||
public bool LoadingData { get; set; }
|
||||
|
||||
SearchResult<TagSearchItem>? searchResults;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
//await UpdateDataAsync(true);
|
||||
}
|
||||
[Inject]
|
||||
ModalService ModalService { get; set; } = default!;
|
||||
|
||||
public async Task OnKeywordsChanged(string? newKeywords)
|
||||
{
|
||||
Keywords = newKeywords;
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
public async Task OnPaginationChange(PaginationEventArgs args)
|
||||
await UpdateAsync(State with
|
||||
{
|
||||
bool resetPageNumber = PageSize != args.PageSize;
|
||||
|
||||
PageNumber = args.Page;
|
||||
PageSize = args.PageSize;
|
||||
|
||||
await UpdateDataAsync(resetPageNumber);
|
||||
Keywords = newKeywords,
|
||||
PageNumber = 1
|
||||
});
|
||||
}
|
||||
|
||||
public async Task OnPageNumberChanged(int newPageNumber)
|
||||
{
|
||||
PageNumber = newPageNumber;
|
||||
await UpdateDataAsync(false);
|
||||
}
|
||||
|
||||
public async Task OnPageSizeChanged(int newPageSize)
|
||||
{
|
||||
PageSize = newPageSize;
|
||||
await UpdateDataAsync(true);
|
||||
}
|
||||
|
||||
private async Task UpdateDataAsync(bool resetPageNumber)
|
||||
{
|
||||
if (resetPageNumber)
|
||||
PageNumber = 1;
|
||||
|
||||
await LoadTagsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadTagsAsync()
|
||||
{
|
||||
LoadingData = true;
|
||||
|
||||
SearchTagsRequest request = new(
|
||||
Options: new()
|
||||
{
|
||||
PageNumber = PageNumber,
|
||||
PageSize = PageSize,
|
||||
Criteria = new()
|
||||
{
|
||||
Name = Keywords
|
||||
},
|
||||
SortOptions = [.. _sortOptions]
|
||||
}
|
||||
);
|
||||
|
||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
||||
var result = await Client.SearchAsync(request);
|
||||
|
||||
searchResults = result?.Results ?? new();
|
||||
|
||||
LoadingData = false;
|
||||
|
||||
//await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private List<SortOption<TagSortField>> _sortOptions =
|
||||
[
|
||||
new(TagSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
||||
];
|
||||
|
||||
private async Task HandleTableChange(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
||||
{
|
||||
// PageNumber = queryModel.PageIndex;
|
||||
// PageSize = queryModel.PageSize;
|
||||
if (IsLoading)
|
||||
return;
|
||||
|
||||
_sortOptions = MapSortOptions(queryModel);
|
||||
var nextSortOptions = MapSortOptions(queryModel);
|
||||
|
||||
await LoadTagsAsync();
|
||||
if (SortOptionsEqual(nextSortOptions, State.SortOptions))
|
||||
return;
|
||||
|
||||
await UpdateAsync(State with
|
||||
{
|
||||
SortOptions = nextSortOptions,
|
||||
PageNumber = 1
|
||||
});
|
||||
}
|
||||
|
||||
private List<SortOption<TagSortField>> MapSortOptions(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
||||
@@ -280,4 +253,126 @@
|
||||
|
||||
NavigationManager.NavigateTo(uri);
|
||||
}
|
||||
|
||||
private async Task SetStatus(TagSearchItem item, TagStatus status)
|
||||
{
|
||||
UpdateTagStatusRequest request = new(
|
||||
TagId: item.TagId,
|
||||
TagStatus: status
|
||||
);
|
||||
|
||||
UpdateTagStatusResponse? response = await Client.UpdateTagStatusAsync(request);
|
||||
|
||||
if (response is not null)
|
||||
{
|
||||
item.Favorite = response.TagStatus is TagStatus.Favorite;
|
||||
item.Blacklisted = response.TagStatus is TagStatus.Blacklisted;
|
||||
}
|
||||
|
||||
//await InvokeAsync(StateHasChanged);
|
||||
|
||||
MessageConfig messageConfig = new()
|
||||
{
|
||||
Content = $"Tag '{item.Name}' set to {status.ToString()}.",
|
||||
Type = MessageType.Success
|
||||
};
|
||||
|
||||
_ = MessageService.OpenAsync(messageConfig);
|
||||
}
|
||||
|
||||
private async Task OpenSetEnglishNameModal(TagSearchItem item)
|
||||
{
|
||||
SetEnglishTagNameModel model = new()
|
||||
{
|
||||
TagName = item.Name,
|
||||
CurrentEnglishTagName = item.EnglishName,
|
||||
UpdatedEnglishTagName = item.EnglishName
|
||||
};
|
||||
|
||||
ModalOptions modalOptions = new()
|
||||
{
|
||||
Title = $"Set English Name: {item.Name}",
|
||||
MaskClosable = false,
|
||||
Centered = true,
|
||||
OkText = "Save",
|
||||
OnOk = async (args) =>
|
||||
{
|
||||
await SetEnglishName(item, model.UpdatedEnglishTagName);
|
||||
}
|
||||
};
|
||||
|
||||
ModalService.CreateModal<TagEnglishNameModal, SetEnglishTagNameModel>(modalOptions, model);
|
||||
}
|
||||
|
||||
private async Task SetEnglishName(TagSearchItem item, string? englishName)
|
||||
{
|
||||
SetTagEnglishNameRequest request = new(
|
||||
TagId: item.TagId,
|
||||
EnglishName: englishName ?? string.Empty
|
||||
);
|
||||
|
||||
SetTagEnglishNameResponse? response = await Client.SetTagEnglishNameAsync(request);
|
||||
|
||||
if (response is null)
|
||||
return;
|
||||
|
||||
item.EnglishName = response.EnglishName;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// await NotificationService.Open(new NotificationConfig
|
||||
// {
|
||||
// Message = "Tag English Name Updated",
|
||||
// Description = $"Tag '{item.Name}' English name set to '{response.EnglishName}'.",
|
||||
// Placement = NotificationPlacement.Top
|
||||
// });
|
||||
|
||||
MessageConfig messageConfig = new()
|
||||
{
|
||||
Content = $"Tag '{item.Name}' English name set to '{response.EnglishName}'.",
|
||||
Type = MessageType.Success
|
||||
};
|
||||
|
||||
_ = MessageService.OpenAsync(messageConfig);
|
||||
}
|
||||
|
||||
protected override TagFilterState ParseStateFromUri(string absoluteUri)
|
||||
=> TagFilterState.FromQuery(new Uri(absoluteUri).Query);
|
||||
|
||||
protected override string BuildUri(TagFilterState state)
|
||||
{
|
||||
var basePath = new Uri(Nav.Uri).GetLeftPart(UriPartial.Path);
|
||||
return QueryHelpers.AddQueryString(basePath, state.ToQuery());
|
||||
}
|
||||
|
||||
protected override bool IsThisPage(string absoluteUri)
|
||||
=> Nav.ToBaseRelativePath(absoluteUri).StartsWith("tags", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
protected override async Task<SearchResult<TagSearchItem>> ExecuteSearchAsync(TagFilterState state, CancellationToken ct)
|
||||
{
|
||||
var response = await Client.SearchAsync(state.ToSearchRequest(), ct);
|
||||
return response?.Results ?? new SearchResult<TagSearchItem>();
|
||||
}
|
||||
|
||||
private AntDesign.SortDirection? GetAntSortDirection(TagSortField field)
|
||||
{
|
||||
var sort = State.SortOptions.FirstOrDefault(x => x.Field == field);
|
||||
|
||||
return sort?.Direction switch
|
||||
{
|
||||
Application.Common.Search.SortDirection.Ascending => AntDesign.SortDirection.Ascending,
|
||||
Application.Common.Search.SortDirection.Descending => AntDesign.SortDirection.Descending,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static bool SortOptionsEqual(
|
||||
IReadOnlyList<SortOption<TagSortField>> left,
|
||||
IReadOnlyList<SortOption<TagSortField>> right)
|
||||
{
|
||||
return left.Count == right.Count
|
||||
&& left.Zip(right).All(x =>
|
||||
x.First.Field == x.Second.Field &&
|
||||
x.First.Direction == x.Second.Direction);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
<h3>Voice Works</h3>
|
||||
|
||||
<VoiceWorkFilters Value="@State" ValueChanged="UpdateAsync" />
|
||||
<JProductCollection Products="Result?.Items"></JProductCollection>
|
||||
<JProductCollection Products="Result?.Items" ProductDeleted="OnProductDeleted"></JProductCollection>
|
||||
|
||||
@if (Result is not null)
|
||||
{
|
||||
@@ -69,4 +69,9 @@
|
||||
|
||||
protected override Task<SearchResult<VoiceWorkSearchResult>> ExecuteSearchAsync(VoiceWorkFilterState state, CancellationToken ct)
|
||||
=> Client.SearchAsync(state.ToSearchRequest(), ct).ContinueWith(t => t.Result?.Results ?? new SearchResult<VoiceWorkSearchResult>(), ct);
|
||||
|
||||
private async Task OnProductDeleted()
|
||||
{
|
||||
_ = RunSearchAsync(false);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
|
||||
using JSMR.Application.Creators.Queries.Search;
|
||||
using JSMR.Application.Tags.Commands.SetEnglishName;
|
||||
using JSMR.Application.Tags.Commands.UpdateTagStatus;
|
||||
using JSMR.Application.Tags.Queries.Search;
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||
using JSMR.UI.Blazor.Exceptions;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -18,6 +24,18 @@ public class VoiceWorksClient(HttpClient http)
|
||||
}
|
||||
};
|
||||
|
||||
private static async Task<T?> ReadJsonOrThrowAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
string body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
throw new ApiException(response.StatusCode, $"Request failed: {(int)response.StatusCode}", body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<SearchVoiceWorksResponse?> SearchAsync(SearchVoiceWorksRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct);
|
||||
@@ -41,4 +59,35 @@ public class VoiceWorksClient(HttpClient http)
|
||||
using var resp = await http.PostAsJsonAsync("/api/tags/search", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SearchTagsResponse>(JsonOptions, cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<SetVoiceWorkFavoriteResponse?> SetVoiceWorkFavoriteeAsync(SetVoiceWorkFavoriteRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/voicework/set-favorite", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SetVoiceWorkFavoriteResponse>(JsonOptions, cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<DeleteVoiceWorkResponse?> DeleteVoiceWorkAsync(DeleteVoiceWorkRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/voicework/delete", request, ct);
|
||||
//return await resp.Content.ReadFromJsonAsync<DeleteVoiceWorkResponse>(JsonOptions, cancellationToken: ct);
|
||||
return await ReadJsonOrThrowAsync<DeleteVoiceWorkResponse>(resp, ct);
|
||||
}
|
||||
|
||||
public async Task<UpdateTagStatusResponse?> UpdateTagStatusAsync(UpdateTagStatusRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/tags/update-status", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<UpdateTagStatusResponse>(JsonOptions, cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<SetTagEnglishNameResponse?> SetTagEnglishNameAsync(SetTagEnglishNameRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/tags/set-english-name", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SetTagEnglishNameResponse>(JsonOptions, cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<UpdateCreatorStatusResponse?> UpdateCreatorStatusAsync(UpdateCreatorStatusRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/creators/update-status", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<UpdateCreatorStatusResponse>(JsonOptions, cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,19 @@ public abstract class SearchPageBase<TState, TItem> : ComponentBase, IAsyncDispo
|
||||
_ = RunSearchAsync();
|
||||
}
|
||||
|
||||
//protected async Task Update2Async(TState next, bool scrollToTop)
|
||||
//{
|
||||
// if (Equals(next, State))
|
||||
// return;
|
||||
|
||||
// Console.WriteLine("Got here");
|
||||
|
||||
// State = next;
|
||||
// NavigateToState(next);
|
||||
|
||||
// _ = RunSearchAsync(scrollToTop);
|
||||
//}
|
||||
|
||||
private void NavigateToState(TState next)
|
||||
{
|
||||
string uri = BuildUri(next);
|
||||
@@ -73,14 +86,17 @@ public abstract class SearchPageBase<TState, TItem> : ComponentBase, IAsyncDispo
|
||||
_ = RunSearchAsync();
|
||||
}
|
||||
|
||||
private async Task RunSearchAsync()
|
||||
protected async Task RunSearchAsync(bool scrollTotop = true)
|
||||
{
|
||||
if (scrollTotop)
|
||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
||||
|
||||
Console.WriteLine("Got here 2");
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
//StateHasChanged();
|
||||
StateHasChanged();
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
51
JSMR.UI.Blazor/wwwroot/css/ant-design.css
Normal file
51
JSMR.UI.Blazor/wwwroot/css/ant-design.css
Normal file
@@ -0,0 +1,51 @@
|
||||
/* Modals */
|
||||
.ant-modal-content {
|
||||
background-color: var(--ant-modal-content-bg);
|
||||
border-radius: var(--ant-border-radius-lg);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-top-color: rgb(83, 99, 109);
|
||||
border-left-color: rgb(72, 88, 99);
|
||||
border-right-color: rgb(72, 88, 99);
|
||||
border-bottom-color: rgb(63, 78, 88);
|
||||
}
|
||||
|
||||
.ant-modal-confirm-title {
|
||||
color: var(--ant-modal-title-color);
|
||||
text-shadow: 1px 1px 2px black;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-body .ant-modal-confirm-content {
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.ant-btn {
|
||||
/*font-weight: var(--ant-button-font-weight);
|
||||
border-width: var(--ant-btn-border-width);
|
||||
border-style: var(--ant-btn-border-style);
|
||||
border-color: var(--ant-btn-border-color);
|
||||
color: var(--ant-btn-text-color);
|
||||
background-color: var(--ant-btn-bg-color);
|
||||
border-radius: var(--ant-border-radius);*/
|
||||
}
|
||||
|
||||
/*
|
||||
--ant-button-font-weight: 400;
|
||||
--ant-button-icon-gap: 8px;
|
||||
--ant-button-padding-inline: 15px;
|
||||
--ant-button-default-border-color: rgba(180, 200, 214, 0.25);
|
||||
--ant-button-content-font-size: 14px;
|
||||
--ant-btn-color-base: var(--ant-button-default-border-color);
|
||||
--ant-btn-text-color: var(--ant-button-default-color);
|
||||
--ant-btn-text-color-hover: var(--ant-button-default-hover-color);
|
||||
--ant-btn-shadow: var(--ant-button-default-shadow);
|
||||
--ant-btn-border-color: var(--ant-btn-color-base);
|
||||
--ant-btn-border-color-hover: var(--ant-btn-color-hover);
|
||||
--ant-btn-border-color-active: var(--ant-btn-color-active);
|
||||
--ant-btn-bg-color: var(--ant-btn-bg-color-container);
|
||||
--ant-btn-text-color: var(--ant-btn-color-base);
|
||||
--ant-btn-text-color-hover: var(--ant-btn-color-hover);
|
||||
--ant-btn-text-color-active: var(--ant-btn-color-active);
|
||||
|
||||
*/
|
||||
@@ -669,11 +669,21 @@ code {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.j-chip.is-clickable {
|
||||
.j-chip-icon-only {
|
||||
padding: .75em;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
.j-chip-thick-border,
|
||||
.j-chip.varient-outlined.j-chip-thick-border {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.j-chip.is-clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background .2s linear, color .2s linear, border-color .2s linear, filter .2s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.j-chip.is-clickable:hover {
|
||||
filter: brightness(1.2);
|
||||
@@ -770,6 +780,10 @@ code {
|
||||
--chip-fg-rgb: var(--rgb-on-yellow, 255 255 255);
|
||||
}
|
||||
|
||||
.j-chip.color-orange {
|
||||
--chip-rgb: var(--rgb-orange);
|
||||
}
|
||||
|
||||
.j-chip.color-pink {
|
||||
color: rgb(var(--chip-fg-rgb));
|
||||
--chip-rgb: var(--rgb-pink);
|
||||
@@ -955,6 +969,14 @@ code {
|
||||
mask-image: url("../svg/microphone-fill.svg");
|
||||
}
|
||||
|
||||
.j-icon-pencil {
|
||||
mask-image: url("../svg/pencil.svg");
|
||||
}
|
||||
|
||||
.j-icon-pencil-fill {
|
||||
mask-image: url("../svg/pencil-fill.svg");
|
||||
}
|
||||
|
||||
.j-icon-2 {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
--rgb-blue: 115 196 255;
|
||||
--rgb-yellow: 255 224 115;
|
||||
--rgb-on-yellow: 0 0 0;
|
||||
--rgb-orange: 255 167 115;
|
||||
--rgb-pink: 224 104 148;
|
||||
--rgb-on-pink: 255 255 255;
|
||||
--rgb-red: 224 104 104;
|
||||
@@ -74,6 +75,47 @@
|
||||
--surface-container-outline-high: rgb(83, 99, 109);
|
||||
--surface-container-outline: rgb(72, 88, 99);
|
||||
--surface-container-outline-low: rgb(63, 78, 88);
|
||||
/* Ant Design - Core */
|
||||
--ant-border-radius: 12px;
|
||||
--ant-line-width: 1px;
|
||||
/* Ant Design - Modals */
|
||||
--ant-color-text: #b4c8d6;
|
||||
--ant-modal-content-bg: #273f50;
|
||||
--ant-modal-title-color: #b4c8d6;
|
||||
/* Ant Design - Buttons */
|
||||
/* Button Part I */
|
||||
--ant-btn-text-color: var(--ant-button-default-color);
|
||||
--ant-btn-text-color-hover: var(--ant-button-default-hover-color);
|
||||
--ant-btn-text-color-active: var(--ant-button-default-active-color);
|
||||
--ant-btn-bg-color-container: var(--ant-button-default-bg);
|
||||
--ant-btn-bg-color-hover: var(--ant-button-default-hover-bg);
|
||||
--ant-btn-bg-color-active: var(--ant-button-default-active-bg);
|
||||
/* Part II */
|
||||
--ant-button-default-bg: #1e3545;
|
||||
--ant-button-default-border-color: rgba(180, 200, 214, 0.25);
|
||||
--ant-button-font-weight: 400;
|
||||
--ant-button-icon-gap: 8px;
|
||||
--ant-button-padding-inline: 15px;
|
||||
--ant-button-content-font-size: 14px;
|
||||
--ant-border-radius-lg: 16px;
|
||||
--ant-button-font-weight: 400;
|
||||
--ant-btn-border-width: var(--ant-line-width);
|
||||
--ant-btn-border-color: #000;
|
||||
--ant-btn-border-color-hover: var(--ant-btn-border-color);
|
||||
--ant-btn-border-color-active: var(--ant-btn-border-color);
|
||||
--ant-btn-border-color-disabled: var(--ant-btn-border-color);
|
||||
--ant-btn-border-style: solid;
|
||||
--ant-btn-text-color: #000;
|
||||
--ant-btn-text-color-hover: var(--ant-btn-text-color);
|
||||
--ant-btn-text-color-active: var(--ant-btn-text-color);
|
||||
--ant-btn-text-color-disabled: var(--ant-btn-text-color);
|
||||
--ant-btn-border-color: var(--ant-btn-color-base);
|
||||
--ant-btn-border-color-hover: var(--ant-btn-color-hover);
|
||||
--ant-btn-border-color-active: var(--ant-btn-color-active);
|
||||
--ant-btn-bg-color: var(--ant-btn-bg-color-container);
|
||||
--ant-btn-text-color: var(--ant-btn-color-base);
|
||||
--ant-btn-text-color-hover: var(--ant-btn-color-hover);
|
||||
--ant-btn-text-color-active: var(--ant-btn-color-active);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<link rel="stylesheet" href="css/radzen.css" />
|
||||
<link href="_content/Bit.BlazorUI/styles/bit.blazorui.css" rel="stylesheet" />
|
||||
<link href="_content/AntDesign/css/ant-design-blazor.dark.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/ant-design.css" />
|
||||
<link rel="stylesheet" href="css/bit-blazor.css" />
|
||||
<link rel="stylesheet" href="css/font.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
|
||||
3
JSMR.UI.Blazor/wwwroot/svg/pencil-fill.svg
Normal file
3
JSMR.UI.Blazor/wwwroot/svg/pencil-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-fill" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.5.5 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 561 B |
3
JSMR.UI.Blazor/wwwroot/svg/pencil.svg
Normal file
3
JSMR.UI.Blazor/wwwroot/svg/pencil.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
@@ -26,15 +26,15 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.1.0" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.55.2" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.7" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -59,7 +59,10 @@ builder.Services
|
||||
|
||||
builder.Services.AddDbContextFactory<AppDbContext>(optionsBuilder =>
|
||||
optionsBuilder
|
||||
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
|
||||
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mySqlOptions =>
|
||||
{
|
||||
mySqlOptions.CommandTimeout(120);
|
||||
})
|
||||
.EnableSensitiveDataLogging(false));
|
||||
|
||||
// Worker services
|
||||
|
||||
Reference in New Issue
Block a user