Compare commits
35 Commits
a45f08fe6d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 53f1df780c | |||
| 53ec67f99d | |||
| 0ed3bc6298 | |||
| 8a13f282b1 | |||
| 5c27fb7f21 | |||
| 06d5aa345d | |||
| 5eecba7eec | |||
| 9c9e33ebec | |||
| 2bd7e3b970 | |||
| abcc82437f | |||
| f6674e0382 | |||
| 77a02a543d | |||
| 2355d7fe65 | |||
| 204e186354 | |||
| dbed9fc905 | |||
| d6a4015c91 | |||
| b63a89c8be | |||
| dfa840d816 | |||
| 6bc91b293d | |||
| da33973229 | |||
| c203b2cbdb | |||
| 1f91e46527 | |||
| be466b52e0 | |||
| b4863a9edf | |||
| b13340061f | |||
| 85a28a6017 | |||
| 45a8c8be5a | |||
| 347f6f297d | |||
| adfbf654a6 | |||
| 0dd11e6351 | |||
| d9e421178f | |||
| 1c016ac62e | |||
| ce9fbe491d | |||
| 22d5a261c5 | |||
| c8403e0e21 |
@@ -8,11 +8,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
|
||||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" 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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ public static class ServiceCollectionExtensions
|
|||||||
?? throw new InvalidOperationException("Missing ConnectionStrings:AppDb");
|
?? throw new InvalidOperationException("Missing ConnectionStrings:AppDb");
|
||||||
|
|
||||||
services.AddDbContextFactory<AppDbContext>(opt =>
|
services.AddDbContextFactory<AppDbContext>(opt =>
|
||||||
opt.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
|
opt.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mySqlOptions =>
|
||||||
|
{
|
||||||
|
mySqlOptions.CommandTimeout(120);
|
||||||
|
})
|
||||||
.EnableSensitiveDataLogging(false));
|
.EnableSensitiveDataLogging(false));
|
||||||
|
|
||||||
services.AddControllers();
|
services.AddControllers();
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using JSMR.Application.Circles.Queries.Search;
|
using JSMR.Application.Circles.Queries.Search;
|
||||||
|
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
|
||||||
using JSMR.Application.Creators.Queries.Search;
|
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.Tags.Queries.Search;
|
||||||
using JSMR.Application.Users;
|
using JSMR.Application.Users;
|
||||||
|
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||||
|
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
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;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +78,9 @@ public static class WebApplicationExtensions
|
|||||||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||||
|
|
||||||
app.MapSearchEndpoints();
|
app.MapSearchEndpoints();
|
||||||
|
app.MapVoiceWorkCommandEndpoints();
|
||||||
|
app.MapTagCommandEndpoints();
|
||||||
|
app.MapCreatorCommandEndpoints();
|
||||||
app.MapAuthenticationEndpoints();
|
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)
|
private static void MapAuthenticationEndpoints(this WebApplication app)
|
||||||
{
|
{
|
||||||
app.MapPost("/auth/login", async (LoginRequest req, IUserRepository users, HttpContext http) =>
|
app.MapPost("/auth/login", async (LoginRequest req, IUserRepository users, HttpContext http) =>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
public record CreatorSearchItem
|
public record CreatorSearchItem
|
||||||
{
|
{
|
||||||
public int CreatorId { get; init; }
|
public int CreatorId { get; set; }
|
||||||
public required string Name { get; init; }
|
public required string Name { get; set; }
|
||||||
public bool Favorite { get; init; }
|
public bool Favorite { get; set; }
|
||||||
public bool Blacklisted { get; init; }
|
public bool Blacklisted { get; set; }
|
||||||
public int VoiceWorkCount { get; init; }
|
public int VoiceWorkCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
using JSMR.Application.Circles.Queries.Search;
|
using JSMR.Application.Circles.Queries.Search;
|
||||||
|
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
|
||||||
using JSMR.Application.Creators.Queries.Search;
|
using JSMR.Application.Creators.Queries.Search;
|
||||||
using JSMR.Application.Scanning;
|
using JSMR.Application.Scanning;
|
||||||
using JSMR.Application.Tags.Commands.SetEnglishName;
|
using JSMR.Application.Tags.Commands.SetEnglishName;
|
||||||
using JSMR.Application.Tags.Commands.UpdateTagStatus;
|
using JSMR.Application.Tags.Commands.UpdateTagStatus;
|
||||||
using JSMR.Application.Tags.Queries.Search;
|
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.Application.VoiceWorks.Queries.Search;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -17,6 +20,9 @@ public static class ApplicationServiceCollectionExtensions
|
|||||||
services.AddScoped<SearchCirclesHandler>();
|
services.AddScoped<SearchCirclesHandler>();
|
||||||
|
|
||||||
services.AddScoped<SearchVoiceWorksHandler>();
|
services.AddScoped<SearchVoiceWorksHandler>();
|
||||||
|
services.AddScoped<SetVoiceWorkFavoriteHandler>();
|
||||||
|
services.AddScoped<DeleteVoiceWorkHandler>();
|
||||||
|
|
||||||
services.AddScoped<ScanVoiceWorksHandler>();
|
services.AddScoped<ScanVoiceWorksHandler>();
|
||||||
|
|
||||||
services.AddScoped<SearchTagsHandler>();
|
services.AddScoped<SearchTagsHandler>();
|
||||||
@@ -24,6 +30,7 @@ public static class ApplicationServiceCollectionExtensions
|
|||||||
services.AddScoped<UpdateTagStatusHandler>();
|
services.AddScoped<UpdateTagStatusHandler>();
|
||||||
|
|
||||||
services.AddScoped<SearchCreatorsHandler>();
|
services.AddScoped<SearchCreatorsHandler>();
|
||||||
|
services.AddScoped<UpdateCreatorStatusHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using JSMR.Application.Integrations.Chobit.Models;
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
|
|
||||||
namespace JSMR.Application.Integrations.Ports;
|
namespace JSMR.Application.Integrations.Chobit.Ports;
|
||||||
|
|
||||||
public interface IChobitClient
|
public interface IChobitClient
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using JSMR.Application.Integrations.Cien.Models;
|
using JSMR.Application.Integrations.Cien.Models;
|
||||||
|
|
||||||
namespace JSMR.Application.Integrations.Ports;
|
namespace JSMR.Application.Integrations.Cien.Ports;
|
||||||
|
|
||||||
public interface ICienClient
|
public interface ICienClient
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
public record ReleasedWork
|
||||||
|
{
|
||||||
|
public required string ProductId { get; init; }
|
||||||
|
public required string Title { get; init; }
|
||||||
|
public required string MaskedTitle { get; init; }
|
||||||
|
public required string Description { get; init; }
|
||||||
|
public required string MaskedDescription { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using JSMR.Application.Enums;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
public record ReleasedWorksRequest(
|
||||||
|
Locale Locale,
|
||||||
|
DateOnly Date,
|
||||||
|
int Period
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
public class ReleasedWorksCollection : Dictionary<string, ReleasedWork>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
10
JSMR.Application/Integrations/DLSite/Ports/IDLSiteClient.cs
Normal file
10
JSMR.Application/Integrations/DLSite/Ports/IDLSiteClient.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
|
||||||
|
public interface IDLSiteClient
|
||||||
|
{
|
||||||
|
Task<VoiceWorkDetailCollection> GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default);
|
||||||
|
Task<ReleasedWorksCollection> GetReleasedWorksAsync(ReleasedWorksRequest request, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using JSMR.Application.Integrations.DLSite.Models;
|
|
||||||
|
|
||||||
namespace JSMR.Application.Integrations.Ports;
|
|
||||||
|
|
||||||
public interface IDLSiteClient
|
|
||||||
{
|
|
||||||
Task<VoiceWorkDetailCollection> GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
10
JSMR.Application/Jobs/IJobProgressWriter.cs
Normal file
10
JSMR.Application/Jobs/IJobProgressWriter.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace JSMR.Application.Jobs;
|
||||||
|
|
||||||
|
public interface IJobProgressWriter
|
||||||
|
{
|
||||||
|
Task SetStepAsync(int jobId, string step, CancellationToken cancellationToken);
|
||||||
|
Task SetProgressAsync(int jobId, int? current, int? total, CancellationToken cancellationToken);
|
||||||
|
Task SetHeartbeatAsync(int jobId, CancellationToken cancellationToken);
|
||||||
|
Task CompleteAsync(int jobId, string? summary, CancellationToken cancellationToken);
|
||||||
|
Task FailAsync(int jobId, string error, CancellationToken cancellationTokenct);
|
||||||
|
}
|
||||||
15
JSMR.Application/Jobs/IJobRepository.cs
Normal file
15
JSMR.Application/Jobs/IJobRepository.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using JSMR.Domain.Entities;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Jobs;
|
||||||
|
|
||||||
|
public interface IJobRepository
|
||||||
|
{
|
||||||
|
Task<Job> AddAsync(Job job, CancellationToken cancellationToken);
|
||||||
|
Task<Job?> GetByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<IReadOnlyList<Job>> GetRecentAsync(int take, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<bool> AnyRunningAsync(CancellationToken cancellationToken);
|
||||||
|
Task<Job?> TryClaimNextQueuedAsync(string workerName, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -4,11 +4,8 @@ namespace JSMR.Application.Scanning.Contracts;
|
|||||||
|
|
||||||
public class DLSiteWork
|
public class DLSiteWork
|
||||||
{
|
{
|
||||||
//public DLSiteWorkType Type { get; set; }
|
|
||||||
//public DLSiteWorkCategory Category { get; set; }
|
|
||||||
public required string ProductName { get; set; }
|
public required string ProductName { get; set; }
|
||||||
public required string ProductId { get; set; }
|
public required string ProductId { get; set; }
|
||||||
//public DateOnly? AnnouncedDate { get; set; }
|
|
||||||
public DateOnly? ExpectedDate { get; set; }
|
public DateOnly? ExpectedDate { get; set; }
|
||||||
public DateOnly? SalesDate { get; set; }
|
public DateOnly? SalesDate { get; set; }
|
||||||
public int Downloads { get; set; }
|
public int Downloads { get; set; }
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public sealed record VoiceWorkIngest
|
|||||||
public AIGeneration AI { get; init; }
|
public AIGeneration AI { get; init; }
|
||||||
public VoiceWorkSeries? Series { get; init; }
|
public VoiceWorkSeries? Series { get; init; }
|
||||||
public VoiceWorkTranslation? Translation { get; init; }
|
public VoiceWorkTranslation? Translation { get; init; }
|
||||||
|
public VoiceWorkLocalizationIngest[] Localizations { get; init; } = [];
|
||||||
|
|
||||||
public static VoiceWorkIngest From(DLSiteWork work, VoiceWorkDetails? details, ChobitResult? chobit)
|
public static VoiceWorkIngest From(DLSiteWork work, VoiceWorkDetails? details, ChobitResult? chobit)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using JSMR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Scanning.Contracts;
|
||||||
|
|
||||||
|
public sealed class VoiceWorkLocalizationIngest
|
||||||
|
{
|
||||||
|
public Language Language { get; init; }
|
||||||
|
public string? Title { get; init; }
|
||||||
|
public string? Description { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Scanning.Contracts;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Scanning.Ports;
|
||||||
|
|
||||||
|
public interface IReleasedWorksProvider
|
||||||
|
{
|
||||||
|
Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using JSMR.Application.Scanning.Contracts;
|
||||||
|
|
||||||
|
namespace JSMR.Application.Scanning.Ports;
|
||||||
|
|
||||||
|
public interface IVoiceWorkIngestBuilder
|
||||||
|
{
|
||||||
|
Task<VoiceWorkIngest[]> BuildAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -1,23 +1,43 @@
|
|||||||
using JSMR.Application.Common.Caching;
|
using JSMR.Application.Common.Caching;
|
||||||
using JSMR.Application.Integrations.Chobit.Models;
|
|
||||||
using JSMR.Application.Integrations.DLSite.Models;
|
|
||||||
using JSMR.Application.Integrations.Ports;
|
|
||||||
using JSMR.Application.Scanning.Contracts;
|
using JSMR.Application.Scanning.Contracts;
|
||||||
using JSMR.Application.Scanning.Ports;
|
using JSMR.Application.Scanning.Ports;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace JSMR.Application.Scanning;
|
namespace JSMR.Application.Scanning;
|
||||||
|
|
||||||
public sealed class ScanVoiceWorksHandler(
|
public sealed class ScanVoiceWorksHandler(
|
||||||
|
ILogger<ScanVoiceWorksHandler> logger,
|
||||||
IVoiceWorkScannerRepository scannerRepository,
|
IVoiceWorkScannerRepository scannerRepository,
|
||||||
IVoiceWorkUpdaterRepository updaterRepository,
|
IVoiceWorkUpdaterRepository updaterRepository,
|
||||||
IDLSiteClient dlsiteClient,
|
|
||||||
IChobitClient chobitClient,
|
|
||||||
ISpamCircleCache spamCircleCache,
|
ISpamCircleCache spamCircleCache,
|
||||||
|
IVoiceWorkIngestBuilder ingestBuilder,
|
||||||
IVoiceWorkSearchUpdater searchUpdater)
|
IVoiceWorkSearchUpdater searchUpdater)
|
||||||
{
|
{
|
||||||
public async Task<ScanVoiceWorksResponse> HandleAsync(ScanVoiceWorksRequest request, CancellationToken cancellationToken)
|
public async Task<ScanVoiceWorksResponse> HandleAsync(ScanVoiceWorksRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
using IDisposable? scope = logger.BeginScope(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Locale"] = request.Locale,
|
||||||
|
["PageNumber"] = request.PageNumber,
|
||||||
|
["PageSize"] = request.PageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
|
string currentPhase = "initialization";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Starting scan handler for page {PageNumber}, page size {PageSize}, locale {Locale}",
|
||||||
|
request.PageNumber,
|
||||||
|
request.PageSize,
|
||||||
|
request.Locale);
|
||||||
|
|
||||||
|
currentPhase = "resolve_scanner";
|
||||||
IVoiceWorksScanner? scanner = scannerRepository.GetScanner(request.Locale);
|
IVoiceWorksScanner? scanner = scannerRepository.GetScanner(request.Locale);
|
||||||
|
|
||||||
|
currentPhase = "resolve_updater";
|
||||||
IVoiceWorkUpdater? updater = updaterRepository.GetUpdater(request.Locale);
|
IVoiceWorkUpdater? updater = updaterRepository.GetUpdater(request.Locale);
|
||||||
|
|
||||||
if (scanner is null)
|
if (scanner is null)
|
||||||
@@ -26,43 +46,118 @@ public sealed class ScanVoiceWorksHandler(
|
|||||||
if (updater is null)
|
if (updater is null)
|
||||||
throw new InvalidOperationException($"No updater registered for locale {request.Locale}.");
|
throw new InvalidOperationException($"No updater registered for locale {request.Locale}.");
|
||||||
|
|
||||||
|
currentPhase = "load_spam_circle_cache";
|
||||||
|
var spamStopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
string[] excludedMakerIds = await spamCircleCache.GetAsync(cancellationToken);
|
||||||
|
|
||||||
|
spamStopwatch.Stop();
|
||||||
|
logger.LogInformation(
|
||||||
|
"Loaded spam circle cache in {ElapsedMs} ms. ExcludedMakerCount={ExcludedMakerCount}",
|
||||||
|
spamStopwatch.ElapsedMilliseconds,
|
||||||
|
excludedMakerIds.Length);
|
||||||
|
|
||||||
VoiceWorkScanOptions options = new(
|
VoiceWorkScanOptions options = new(
|
||||||
PageNumber: request.PageNumber,
|
PageNumber: request.PageNumber,
|
||||||
PageSize: request.PageSize,
|
PageSize: request.PageSize,
|
||||||
ExcludedMakerIds: await spamCircleCache.GetAsync(cancellationToken),
|
ExcludedMakerIds: excludedMakerIds,
|
||||||
ExcludePartiallyAIGeneratedWorks: true,
|
ExcludePartiallyAIGeneratedWorks: true,
|
||||||
ExcludeAIGeneratedWorks: true
|
ExcludeAIGeneratedWorks: true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
currentPhase = "scan_page";
|
||||||
|
var scanStopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
VoiceWorkScanResult scanResult = await scanner.ScanPageAsync(options, cancellationToken);
|
VoiceWorkScanResult scanResult = await scanner.ScanPageAsync(options, cancellationToken);
|
||||||
|
|
||||||
|
scanStopwatch.Stop();
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Scanned source page in {ElapsedMs} ms. EndOfResults={EndOfResults}, ResultCount={ResultCount}",
|
||||||
|
scanStopwatch.ElapsedMilliseconds,
|
||||||
|
scanResult.EndOfResults,
|
||||||
|
scanResult.Works.Length);
|
||||||
|
|
||||||
|
|
||||||
if (scanResult.EndOfResults)
|
if (scanResult.EndOfResults)
|
||||||
{
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"End of results reached for page {PageNumber}. TotalElapsedMs={ElapsedMs}",
|
||||||
|
request.PageNumber,
|
||||||
|
stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
return new ScanVoiceWorksResponse(
|
return new ScanVoiceWorksResponse(
|
||||||
Results: [],
|
Results: [],
|
||||||
EndOfResults: true
|
EndOfResults: true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
|
currentPhase = "build_ingests";
|
||||||
VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
|
var ingestSw = Stopwatch.StartNew();
|
||||||
ChobitResultCollection chobitResults = await chobitClient.GetSampleInfoAsync(productIds, cancellationToken);
|
|
||||||
|
|
||||||
VoiceWorkIngest[] ingests = [.. scanResult.Works.Select(work =>
|
VoiceWorkIngest[] ingests = await ingestBuilder.BuildAsync(scanResult, cancellationToken);
|
||||||
{
|
|
||||||
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value);
|
ingestSw.Stop();
|
||||||
chobitResults.TryGetValue(work.ProductId, out ChobitResult? chobit);
|
logger.LogInformation(
|
||||||
return VoiceWorkIngest.From(work, value, chobit);
|
"Built ingests in {ElapsedMs} ms. IngestCount={IngestCount}",
|
||||||
})];
|
ingestSw.ElapsedMilliseconds,
|
||||||
|
ingests.Length);
|
||||||
|
|
||||||
|
currentPhase = "upsert_voiceworks";
|
||||||
|
var upsertSw = Stopwatch.StartNew();
|
||||||
|
|
||||||
VoiceWorkUpsertResult[] upsertResults = await updater.UpsertAsync(ingests, cancellationToken);
|
VoiceWorkUpsertResult[] upsertResults = await updater.UpsertAsync(ingests, cancellationToken);
|
||||||
|
|
||||||
|
upsertSw.Stop();
|
||||||
|
|
||||||
int[] voiceWorkIds = [.. upsertResults.Where(x => x.VoiceWorkId.HasValue).Select(x => x.VoiceWorkId!.Value)];
|
int[] voiceWorkIds = [.. upsertResults.Where(x => x.VoiceWorkId.HasValue).Select(x => x.VoiceWorkId!.Value)];
|
||||||
|
|
||||||
|
int issueCount = upsertResults.Sum(x => x.Issues.Count);
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Upserted voice works in {ElapsedMs} ms. UpsertResultCount={UpsertResultCount}, VoiceWorkIdCount={VoiceWorkIdCount}, IssueCount={IssueCount}",
|
||||||
|
upsertSw.ElapsedMilliseconds,
|
||||||
|
upsertResults.Length,
|
||||||
|
voiceWorkIds.Length,
|
||||||
|
issueCount);
|
||||||
|
|
||||||
|
currentPhase = "update_search";
|
||||||
|
var searchSw = Stopwatch.StartNew();
|
||||||
|
|
||||||
await searchUpdater.UpdateAsync(voiceWorkIds, cancellationToken);
|
await searchUpdater.UpdateAsync(voiceWorkIds, cancellationToken);
|
||||||
|
|
||||||
|
searchSw.Stop();
|
||||||
|
logger.LogInformation(
|
||||||
|
"Updated search index in {ElapsedMs} ms. VoiceWorkIdCount={VoiceWorkIdCount}",
|
||||||
|
searchSw.ElapsedMilliseconds,
|
||||||
|
voiceWorkIds.Length);
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Completed scan handler for page {PageNumber} in {ElapsedMs} ms",
|
||||||
|
request.PageNumber,
|
||||||
|
stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
return new ScanVoiceWorksResponse(
|
return new ScanVoiceWorksResponse(
|
||||||
Results: upsertResults,
|
Results: upsertResults,
|
||||||
EndOfResults: false
|
EndOfResults: false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Scan handler failed during phase {Phase} for page {PageNumber} after {ElapsedMs} ms",
|
||||||
|
currentPhase,
|
||||||
|
request.PageNumber,
|
||||||
|
stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
public record TagSearchItem
|
public record TagSearchItem
|
||||||
{
|
{
|
||||||
public int TagId { get; init; }
|
public int TagId { get; set; }
|
||||||
public required string Name { get; init; }
|
public required string Name { get; set; }
|
||||||
public bool Favorite { get; init; }
|
public bool Favorite { get; set; }
|
||||||
public bool Blacklisted { get; init; }
|
public bool Blacklisted { get; set; }
|
||||||
public string? EnglishName { get; init; }
|
public string? EnglishName { get; set; }
|
||||||
public int VoiceWorkCount { get; init; }
|
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;
|
namespace JSMR.Application.VoiceWorks.Ports;
|
||||||
|
|
||||||
public interface IVoiceWorkWriter
|
public interface IVoiceWorkWriter
|
||||||
{
|
{
|
||||||
Task<SetVoiceWorkFavoriteResponse> SetFavoriteAsync(SetVoiceWorkFavoriteRequest request, CancellationToken cancellationToken);
|
Task<SetVoiceWorkFavoriteResponse> SetFavoriteAsync(SetVoiceWorkFavoriteRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<DeleteVoiceWorkResponse> DeleteAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -4,32 +4,34 @@ namespace JSMR.Application.VoiceWorks.Queries.Search;
|
|||||||
|
|
||||||
public record VoiceWorkSearchResult
|
public record VoiceWorkSearchResult
|
||||||
{
|
{
|
||||||
public int VoiceWorkId { get; init; }
|
public int VoiceWorkId { get; set; }
|
||||||
public required string ProductId { get; init; }
|
public required string ProductId { get; set; }
|
||||||
public string? OriginalProductId { get; init; }
|
public string? OriginalProductId { get; set; }
|
||||||
public string? Description { get; init; }
|
public string? Description { get; set; }
|
||||||
public required string ProductName { get; init; }
|
public required string ProductName { get; set; }
|
||||||
public required string ProductUrl { get; init; }
|
public required string ProductUrl { get; set; }
|
||||||
public bool HasImage { get; init; }
|
public bool HasImage { get; set; }
|
||||||
public required string Maker { get; init; }
|
public required string Maker { get; set; }
|
||||||
public required string MakerId { get; init; }
|
public required string MakerId { get; set; }
|
||||||
public DateTime? ExpectedDate { get; init; }
|
public DateTime? ExpectedDate { get; set; }
|
||||||
public DateTime? SalesDate { get; init; }
|
public DateTime? SalesDate { get; set; }
|
||||||
public DateTime? PlannedReleaseDate { get; init; }
|
public DateTime? PlannedReleaseDate { get; set; }
|
||||||
public int? Downloads { get; init; }
|
public int? Downloads { get; set; }
|
||||||
public int? WishlistCount { get; init; }
|
public int? WishlistCount { get; set; }
|
||||||
public byte? StarRating { get; init; }
|
public byte? StarRating { get; set; }
|
||||||
public int? Votes { get; init; }
|
public int? Votes { get; set; }
|
||||||
public bool HasTrial { get; init; }
|
public bool HasTrial { get; set; }
|
||||||
public bool HasChobit { get; init; }
|
public bool HasChobit { get; set; }
|
||||||
public AgeRating Rating { get; init; }
|
public AgeRating Rating { get; set; }
|
||||||
public bool Favorite { get; init; }
|
public bool Favorite { get; set; }
|
||||||
public byte Status { get; init; }
|
public byte Status { get; set; }
|
||||||
public byte SubtitleLanguage { get; init; }
|
public byte SubtitleLanguage { get; set; }
|
||||||
public bool? IsValid { get; init; }
|
public bool? IsValid { get; set; }
|
||||||
public required VoiceWorkCircleItem Circle { get; set; }
|
public required VoiceWorkCircleItem Circle { get; set; }
|
||||||
|
public VoiceWorkCircleItem? OriginalCircle { get; set; }
|
||||||
public VoiceWorkTagItem[] Tags { get; set; } = [];
|
public VoiceWorkTagItem[] Tags { get; set; } = [];
|
||||||
public VoiceWorkCreatorItem[] Creators { get; set; } = [];
|
public VoiceWorkCreatorItem[] Creators { get; set; } = [];
|
||||||
|
public string[] SupportedLanguages { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VoiceWorkCircleItem
|
public class VoiceWorkCircleItem
|
||||||
|
|||||||
33
JSMR.Domain/Entities/Job.cs
Normal file
33
JSMR.Domain/Entities/Job.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using JSMR.Domain.Enums;
|
||||||
|
|
||||||
|
namespace JSMR.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class Job
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Code { get; set; } = null!;
|
||||||
|
public JobStatus Status { get; set; }
|
||||||
|
|
||||||
|
public string? RequestedByUserId { get; set; }
|
||||||
|
public string RequestedSource { get; set; } = "Manual";
|
||||||
|
|
||||||
|
public string? ParametersJson { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedUtc { get; set; }
|
||||||
|
public DateTime? StartedUtc { get; set; }
|
||||||
|
public DateTime? CompletedUtc { get; set; }
|
||||||
|
public DateTime? HeartbeatUtc { get; set; }
|
||||||
|
|
||||||
|
public string? WorkerName { get; set; }
|
||||||
|
public string? CurrentStep { get; set; }
|
||||||
|
|
||||||
|
public int? ProgressCurrent { get; set; }
|
||||||
|
public int? ProgressTotal { get; set; }
|
||||||
|
|
||||||
|
public string? ResultSummary { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
|
||||||
|
public bool CancellationRequested { get; set; }
|
||||||
|
public int AttemptCount { get; set; }
|
||||||
|
}
|
||||||
10
JSMR.Domain/Enums/JobStatus.cs
Normal file
10
JSMR.Domain/Enums/JobStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace JSMR.Domain.Enums;
|
||||||
|
|
||||||
|
public enum JobStatus
|
||||||
|
{
|
||||||
|
Queued = 0,
|
||||||
|
Running = 1,
|
||||||
|
Succeeded = 2,
|
||||||
|
Failed = 3,
|
||||||
|
Cancelled = 4
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ using JSMR.Application.Common.Caching;
|
|||||||
using JSMR.Application.Creators.Ports;
|
using JSMR.Application.Creators.Ports;
|
||||||
using JSMR.Application.Creators.Queries.Search.Ports;
|
using JSMR.Application.Creators.Queries.Search.Ports;
|
||||||
using JSMR.Application.Enums;
|
using JSMR.Application.Enums;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.Chobit.Ports;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
using JSMR.Application.Jobs;
|
||||||
using JSMR.Application.Scanning.Ports;
|
using JSMR.Application.Scanning.Ports;
|
||||||
using JSMR.Application.Tags.Ports;
|
using JSMR.Application.Tags.Ports;
|
||||||
using JSMR.Application.Tags.Queries.Search.Ports;
|
using JSMR.Application.Tags.Queries.Search.Ports;
|
||||||
@@ -19,6 +21,7 @@ using JSMR.Infrastructure.Common.SupportedLanguages;
|
|||||||
using JSMR.Infrastructure.Common.Time;
|
using JSMR.Infrastructure.Common.Time;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Circles;
|
using JSMR.Infrastructure.Data.Repositories.Circles;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Creators;
|
using JSMR.Infrastructure.Data.Repositories.Creators;
|
||||||
|
using JSMR.Infrastructure.Data.Repositories.Jobs;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Tags;
|
using JSMR.Infrastructure.Data.Repositories.Tags;
|
||||||
using JSMR.Infrastructure.Data.Repositories.Users;
|
using JSMR.Infrastructure.Data.Repositories.Users;
|
||||||
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||||
@@ -50,6 +53,9 @@ public static class InfrastructureServiceCollectionExtensions
|
|||||||
services.AddKeyedScoped<IVoiceWorksScanner, EnglishVoiceWorksScanner>(Locale.English);
|
services.AddKeyedScoped<IVoiceWorksScanner, EnglishVoiceWorksScanner>(Locale.English);
|
||||||
services.AddScoped<IVoiceWorkScannerRepository, VoiceWorkScannerRepository>();
|
services.AddScoped<IVoiceWorkScannerRepository, VoiceWorkScannerRepository>();
|
||||||
|
|
||||||
|
services.AddScoped<IReleasedWorksProvider, ReleasedWorksProvider>();
|
||||||
|
services.AddScoped<IVoiceWorkIngestBuilder, VoiceWorkIngestBuilder>();
|
||||||
|
|
||||||
services.AddKeyedScoped<IVoiceWorkUpdater, VoiceWorkUpdater>(Locale.Japanese);
|
services.AddKeyedScoped<IVoiceWorkUpdater, VoiceWorkUpdater>(Locale.Japanese);
|
||||||
services.AddKeyedScoped<IVoiceWorkUpdater, EnglishVoiceWorkUpdater>(Locale.English);
|
services.AddKeyedScoped<IVoiceWorkUpdater, EnglishVoiceWorkUpdater>(Locale.English);
|
||||||
services.AddScoped<IVoiceWorkUpdaterRepository, VoiceWorkUpdaterRepository>();
|
services.AddScoped<IVoiceWorkUpdaterRepository, VoiceWorkUpdaterRepository>();
|
||||||
@@ -68,6 +74,9 @@ public static class InfrastructureServiceCollectionExtensions
|
|||||||
services.AddScoped<ICreatorSearchProvider, CreatorSearchProvider>();
|
services.AddScoped<ICreatorSearchProvider, CreatorSearchProvider>();
|
||||||
services.AddScoped<ICreatorWriter, CreatorWriter>();
|
services.AddScoped<ICreatorWriter, CreatorWriter>();
|
||||||
|
|
||||||
|
services.AddScoped<IJobRepository, JobRepository>();
|
||||||
|
services.AddScoped<IJobProgressWriter, JobProgressWriter>();
|
||||||
|
|
||||||
services.AddSingleton<ICache, MemoryCacheAdapter>();
|
services.AddSingleton<ICache, MemoryCacheAdapter>();
|
||||||
services.AddSingleton<ISpamCircleCache, SpamCircleCache>();
|
services.AddSingleton<ISpamCircleCache, SpamCircleCache>();
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
|||||||
public DbSet<Series> Series { get; set; }
|
public DbSet<Series> Series { get; set; }
|
||||||
public DbSet<VoiceWorkSearch> VoiceWorkSearches { get; set; }
|
public DbSet<VoiceWorkSearch> VoiceWorkSearches { get; set; }
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<Job> Jobs { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
44
JSMR.Infrastructure/Data/Configuration/JobConfiguration.cs
Normal file
44
JSMR.Infrastructure/Data/Configuration/JobConfiguration.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Data.Configuration;
|
||||||
|
|
||||||
|
public sealed class JobConfiguration : IEntityTypeConfiguration<Job>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Job> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Jobs");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.Property(x => x.Code)
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(x => x.RequestedByUserId)
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder.Property(x => x.RequestedSource)
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(x => x.WorkerName)
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
builder.Property(x => x.CurrentStep)
|
||||||
|
.HasMaxLength(500);
|
||||||
|
|
||||||
|
builder.Property(x => x.ResultSummary)
|
||||||
|
.HasMaxLength(2000);
|
||||||
|
|
||||||
|
builder.Property(x => x.Error)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
builder.Property(x => x.ParametersJson)
|
||||||
|
.HasColumnType("LONGTEXT");
|
||||||
|
|
||||||
|
builder.HasIndex(x => new { x.Status, x.CreatedUtc });
|
||||||
|
builder.HasIndex(x => x.Code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,8 +44,8 @@ public class CreatorSearchProvider(AppDbContext context) : SearchProvider<Creato
|
|||||||
Expression<Func<CreatorSearchItem, object?>> selector = field switch
|
Expression<Func<CreatorSearchItem, object?>> selector = field switch
|
||||||
{
|
{
|
||||||
CreatorSortField.VoiceWorkCount => x => x.VoiceWorkCount,
|
CreatorSortField.VoiceWorkCount => x => x.VoiceWorkCount,
|
||||||
CreatorSortField.Favorite => x => !x.Favorite,
|
CreatorSortField.Favorite => x => x.Favorite,
|
||||||
CreatorSortField.Blacklisted => x => !x.Blacklisted,
|
CreatorSortField.Blacklisted => x => x.Blacklisted,
|
||||||
_ => x => x.Name
|
_ => x => x.Name
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using JSMR.Application.Jobs;
|
||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using JSMR.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Data.Repositories.Jobs;
|
||||||
|
|
||||||
|
public sealed class JobProgressWriter(AppDbContext dbContext) : IJobProgressWriter
|
||||||
|
{
|
||||||
|
public async Task SetStepAsync(int jobId, string step, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.CurrentStep = step;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetProgressAsync(int jobId, int? current, int? total, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.ProgressCurrent = current;
|
||||||
|
job.ProgressTotal = total;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetHeartbeatAsync(int jobId, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteAsync(int jobId, string? summary, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.Status = JobStatus.Succeeded;
|
||||||
|
job.CompletedUtc = DateTime.UtcNow;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
job.ResultSummary = summary;
|
||||||
|
job.CurrentStep = "Completed";
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FailAsync(int jobId, string error, CancellationToken canellationToken)
|
||||||
|
{
|
||||||
|
Job? job = await dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == jobId, canellationToken);
|
||||||
|
|
||||||
|
if (job is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
job.Status = JobStatus.Failed;
|
||||||
|
job.CompletedUtc = DateTime.UtcNow;
|
||||||
|
job.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
job.Error = error;
|
||||||
|
job.CurrentStep = "Failed";
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(canellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
JSMR.Infrastructure/Data/Repositories/Jobs/JobRepository.cs
Normal file
52
JSMR.Infrastructure/Data/Repositories/Jobs/JobRepository.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using JSMR.Application.Jobs;
|
||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using JSMR.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Data.Repositories.Jobs;
|
||||||
|
|
||||||
|
public sealed class JobRepository(AppDbContext dbContext) : IJobRepository
|
||||||
|
{
|
||||||
|
public async Task<Job> AddAsync(Job job, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await dbContext.Jobs.AddAsync(job, cancellationToken);
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Job?> GetByIdAsync(int id, CancellationToken cancellationToken)
|
||||||
|
=> dbContext.Jobs.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Job>> GetRecentAsync(int take, CancellationToken cancellationToken)
|
||||||
|
=> await dbContext.Jobs
|
||||||
|
.OrderByDescending(x => x.CreatedUtc)
|
||||||
|
.Take(take)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
public Task<bool> AnyRunningAsync(CancellationToken cancellationToken)
|
||||||
|
=> dbContext.Jobs.AnyAsync(x => x.Status == JobStatus.Running, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<Job?> TryClaimNextQueuedAsync(string workerName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Job? next = await dbContext.Jobs
|
||||||
|
.Where(x => x.Status == JobStatus.Queued)
|
||||||
|
.OrderBy(x => x.CreatedUtc)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (next is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
next.Status = JobStatus.Running;
|
||||||
|
next.StartedUtc = DateTime.UtcNow;
|
||||||
|
next.HeartbeatUtc = DateTime.UtcNow;
|
||||||
|
next.WorkerName = workerName;
|
||||||
|
next.AttemptCount += 1;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveChangesAsync(CancellationToken ct)
|
||||||
|
=> dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using JSMR.Application.Enums;
|
|||||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||||
using JSMR.Domain.Entities;
|
using JSMR.Domain.Entities;
|
||||||
using JSMR.Domain.Enums;
|
using JSMR.Domain.Enums;
|
||||||
|
using JSMR.Domain.ValueObjects;
|
||||||
using JSMR.Infrastructure.Common.Queries;
|
using JSMR.Infrastructure.Common.Queries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
@@ -60,9 +61,10 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (criteria.SupportedLanguages.Length > 0)
|
//if (criteria.SupportedLanguages.Length > 0)
|
||||||
filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.AsEnumerable().Contains((Language)x.VoiceWork.SubtitleLanguage));
|
// filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.AsEnumerable().Contains((Language)x.VoiceWork.SubtitleLanguage));
|
||||||
|
|
||||||
|
filteredQuery = ApplySupportedLanguageFilter(filteredQuery, criteria);
|
||||||
filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria);
|
filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria);
|
||||||
filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria);
|
filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria);
|
||||||
filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria);
|
filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria);
|
||||||
@@ -118,6 +120,26 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
|||||||
return query.Where(x => voiceWorkIds.Contains(x.VoiceWork.VoiceWorkId));
|
return query.Where(x => voiceWorkIds.Contains(x.VoiceWork.VoiceWorkId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private IQueryable<VoiceWorkQuery> ApplySupportedLanguageFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
|
||||||
|
{
|
||||||
|
if (criteria.SupportedLanguages.Length == 0)
|
||||||
|
return query;
|
||||||
|
|
||||||
|
List<string> languageCodes = [];
|
||||||
|
|
||||||
|
foreach (Language language in criteria.SupportedLanguages)
|
||||||
|
{
|
||||||
|
if (SupportedLanguage.TryFromLanguage(language, out SupportedLanguage? supportedLanguage))
|
||||||
|
languageCodes.Add(supportedLanguage.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageCodes.Count == 0)
|
||||||
|
return query;
|
||||||
|
|
||||||
|
return query.Where(q => context.VoiceWorkSupportedLanguages.Any(sl => sl.VoiceWorkId == q.VoiceWork.VoiceWorkId && languageCodes.Contains(sl.Language)));
|
||||||
|
}
|
||||||
|
|
||||||
private IQueryable<VoiceWorkQuery> ApplyCircleStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
|
private IQueryable<VoiceWorkQuery> ApplyCircleStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
|
||||||
{
|
{
|
||||||
if (criteria.CircleStatus is null)
|
if (criteria.CircleStatus is null)
|
||||||
@@ -432,6 +454,7 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
|||||||
PlannedReleaseDate = voiceWork.PlannedReleaseDate,
|
PlannedReleaseDate = voiceWork.PlannedReleaseDate,
|
||||||
Downloads = voiceWork.Downloads,
|
Downloads = voiceWork.Downloads,
|
||||||
WishlistCount = voiceWork.WishlistCount,
|
WishlistCount = voiceWork.WishlistCount,
|
||||||
|
Rating = (AgeRating)voiceWork.Rating,
|
||||||
Status = voiceWork.Status,
|
Status = voiceWork.Status,
|
||||||
SubtitleLanguage = voiceWork.SubtitleLanguage,
|
SubtitleLanguage = voiceWork.SubtitleLanguage,
|
||||||
HasTrial = voiceWork.HasTrial,
|
HasTrial = voiceWork.HasTrial,
|
||||||
@@ -449,16 +472,24 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
|||||||
|
|
||||||
int[] voiceWorkIds = [.. items.Select(i => i.VoiceWorkId)];
|
int[] voiceWorkIds = [.. items.Select(i => i.VoiceWorkId)];
|
||||||
|
|
||||||
Dictionary<int, VoiceWorkTagItem[]> tagsByVw = await GetTagsAsync(voiceWorkIds, cancellationToken);
|
var tagsByVoiceWork = await GetTagsAsync(voiceWorkIds, cancellationToken);
|
||||||
Dictionary<int, VoiceWorkCreatorItem[]> creatorsByVw = await GetCreatorsAsync(voiceWorkIds, cancellationToken);
|
var creatorsByVoiceWork = await GetCreatorsAsync(voiceWorkIds, cancellationToken);
|
||||||
|
var supportedLanguagesByVoiceWork = await GetSupportedLanguagesAsync(voiceWorkIds, cancellationToken);
|
||||||
|
var originalCirclesByVoiceWork = await GetOriginalCircles(voiceWorkIds, cancellationToken);
|
||||||
|
|
||||||
foreach (VoiceWorkSearchResult item in items)
|
foreach (VoiceWorkSearchResult item in items)
|
||||||
{
|
{
|
||||||
if (tagsByVw.TryGetValue(item.VoiceWorkId, out VoiceWorkTagItem[]? tags))
|
if (tagsByVoiceWork.TryGetValue(item.VoiceWorkId, out VoiceWorkTagItem[]? tags))
|
||||||
item.Tags = tags;
|
item.Tags = tags;
|
||||||
|
|
||||||
if (creatorsByVw.TryGetValue(item.VoiceWorkId, out VoiceWorkCreatorItem[]? creators))
|
if (creatorsByVoiceWork.TryGetValue(item.VoiceWorkId, out VoiceWorkCreatorItem[]? creators))
|
||||||
item.Creators = creators;
|
item.Creators = creators;
|
||||||
|
|
||||||
|
if (supportedLanguagesByVoiceWork.TryGetValue(item.VoiceWorkId, out string[]? supportedLanguages))
|
||||||
|
item.SupportedLanguages = supportedLanguages;
|
||||||
|
|
||||||
|
if (originalCirclesByVoiceWork.TryGetValue(item.VoiceWorkId, out VoiceWorkCircleItem? voiceWorkCircleItem))
|
||||||
|
item.OriginalCircle = voiceWorkCircleItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,4 +530,51 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
|
|||||||
g => g.Select(r => new VoiceWorkCreatorItem { CreatorId = r.CreatorId, Name = r.Name, IsFavorite = r.Favorite, IsBlacklisted = r.Blacklisted }).ToArray()
|
g => g.Select(r => new VoiceWorkCreatorItem { CreatorId = r.CreatorId, Name = r.Name, IsFavorite = r.Favorite, IsBlacklisted = r.Blacklisted }).ToArray()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<int, string[]>> GetSupportedLanguagesAsync(int[] voiceWorkIds, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var supportedLanguageRows = await (
|
||||||
|
from voiceWorkSupportedLanguage in context.VoiceWorkSupportedLanguages.AsNoTracking()
|
||||||
|
where voiceWorkIds.Contains(voiceWorkSupportedLanguage.VoiceWorkId)
|
||||||
|
select new { voiceWorkSupportedLanguage.VoiceWorkId, voiceWorkSupportedLanguage.Language }
|
||||||
|
).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return supportedLanguageRows
|
||||||
|
.GroupBy(r => r.VoiceWorkId)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.Select(r => r.Language).ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<int, VoiceWorkCircleItem>> GetOriginalCircles(int[] voiceWorkIds, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var originalCircleRows = await (
|
||||||
|
from voiceWork in context.VoiceWorks.AsNoTracking()
|
||||||
|
join orignalVoiceWork in context.VoiceWorks.AsNoTracking() on voiceWork.OriginalProductId equals orignalVoiceWork.ProductId
|
||||||
|
join originalCircle in context.Circles.AsNoTracking() on orignalVoiceWork.CircleId equals originalCircle.CircleId
|
||||||
|
where voiceWorkIds.Contains(voiceWork.VoiceWorkId)
|
||||||
|
select new
|
||||||
|
{
|
||||||
|
voiceWork.VoiceWorkId,
|
||||||
|
originalCircle.Name,
|
||||||
|
originalCircle.MakerId,
|
||||||
|
originalCircle.Favorite,
|
||||||
|
originalCircle.Blacklisted
|
||||||
|
}
|
||||||
|
).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return originalCircleRows
|
||||||
|
.GroupBy(r => r.VoiceWorkId)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.Select(r => new VoiceWorkCircleItem()
|
||||||
|
{
|
||||||
|
Name = r.Name,
|
||||||
|
MakerId = r.MakerId,
|
||||||
|
IsFavorite = r.Favorite,
|
||||||
|
IsBlacklisted = r.Blacklisted
|
||||||
|
}).First()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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.Application.VoiceWorks.Ports;
|
||||||
using JSMR.Domain.Entities;
|
using JSMR.Domain.Entities;
|
||||||
using JSMR.Infrastructure.Common.Time;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||||
@@ -18,6 +18,57 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
|
|||||||
return new SetVoiceWorkFavoriteResponse(request.VoiceWorkId, request.IsFavorite);
|
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)
|
private async Task<VoiceWork> GetVoiceWorkAsync(int voiceWorkId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await dbContext.VoiceWorks.FirstOrDefaultAsync(voiceWork => voiceWork.VoiceWorkId == voiceWorkId, cancellationToken)
|
return await dbContext.VoiceWorks.FirstOrDefaultAsync(voiceWork => voiceWork.VoiceWorkId == voiceWorkId, cancellationToken)
|
||||||
|
|||||||
48
JSMR.Infrastructure/Globalization/LocaleMap.cs
Normal file
48
JSMR.Infrastructure/Globalization/LocaleMap.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using JSMR.Application.Enums;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Globalization;
|
||||||
|
|
||||||
|
internal class LocaleMapper
|
||||||
|
{
|
||||||
|
// TODO: Deprecate
|
||||||
|
public static readonly IReadOnlyDictionary<Locale, (string Abbreviation, string Code)> Map =
|
||||||
|
new Dictionary<Locale, (string, string)>
|
||||||
|
{
|
||||||
|
{ Locale.Japanese, ("jp", "ja_JP") },
|
||||||
|
{ Locale.English, ("en", "en_US") },
|
||||||
|
{ Locale.ChineseSimplified, ("zh-cn", "zh_CN") },
|
||||||
|
{ Locale.ChineseTraditional, ("zh-tw", "zh_TW") },
|
||||||
|
{ Locale.Korean, ("ko", "ko_KR") },
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public static string ToDLSiteLocale(Locale locale) => locale switch
|
||||||
|
{
|
||||||
|
Locale.Japanese => "ja-jp",
|
||||||
|
Locale.English => "en-us",
|
||||||
|
Locale.ChineseSimplified => "zh-cn",
|
||||||
|
Locale.ChineseTraditional => "zh-tw",
|
||||||
|
Locale.Korean => "ko-kr",
|
||||||
|
_ => throw new NotSupportedException($"Locale '{locale}' is not supported.")
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToDLSiteApiLocale(Locale locale) => locale switch
|
||||||
|
{
|
||||||
|
Locale.Japanese => "ja_JP",
|
||||||
|
Locale.English => "en_US",
|
||||||
|
Locale.ChineseSimplified => "zh_CN",
|
||||||
|
Locale.ChineseTraditional => "zh_TW",
|
||||||
|
Locale.Korean => "ko_KR",
|
||||||
|
_ => throw new NotSupportedException($"Locale '{locale}' is not supported.")
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToAbbreviation(Locale locale) => locale switch
|
||||||
|
{
|
||||||
|
Locale.Japanese => "jp",
|
||||||
|
Locale.English => "en",
|
||||||
|
Locale.ChineseSimplified => "zh-cn",
|
||||||
|
Locale.ChineseTraditional => "zh-tw",
|
||||||
|
Locale.Korean => "ko",
|
||||||
|
_ => throw new NotSupportedException($"Locale '{locale}' is not supported.")
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,9 +22,22 @@ public abstract class ApiClient(HttpClient http, ILogger logger, JsonSerializerO
|
|||||||
|
|
||||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
return await JsonSerializer.DeserializeAsync<TResponse>(stream, json, cancellationToken).ConfigureAwait(false)
|
return await JsonSerializer.DeserializeAsync<TResponse>(stream, json, cancellationToken).ConfigureAwait(false)
|
||||||
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
|
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
|
||||||
}
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex,
|
||||||
|
"Failed to deserialize JSON from {Url}. ContentLengthHeader={ContentLengthHeader}",
|
||||||
|
url,
|
||||||
|
response.Content.Headers.ContentLength);
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task<TResponse> GetJsonpAsync<TResponse>(
|
protected async Task<TResponse> GetJsonpAsync<TResponse>(
|
||||||
string url,
|
string url,
|
||||||
|
|||||||
@@ -21,11 +21,21 @@ public class VoiceWorkSearchUpdater(AppDbContext dbContext) : IVoiceWorkSearchUp
|
|||||||
.Include(vw => vw.EnglishVoiceWorks)
|
.Include(vw => vw.EnglishVoiceWorks)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
Dictionary<int, VoiceWorkSearch> existingSearches = await dbContext.VoiceWorkSearches
|
||||||
|
.Where(s => voiceWorkIds.Contains(s.VoiceWorkId))
|
||||||
|
.ToDictionaryAsync(s => s.VoiceWorkId, cancellationToken);
|
||||||
|
|
||||||
|
int[] tagIds = [.. batch.SelectMany(vw => vw.Tags).Select(vwt => vwt.TagId).Distinct()];
|
||||||
|
|
||||||
|
Dictionary<int, EnglishTag> englishTags = await dbContext.EnglishTags
|
||||||
|
.Where(et => tagIds.Contains(et.TagId))
|
||||||
|
.ToDictionaryAsync(et => et.TagId, cancellationToken);
|
||||||
|
|
||||||
foreach (var voiceWork in batch)
|
foreach (var voiceWork in batch)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
UpdateSearchText(voiceWork);
|
UpdateSearchText(voiceWork, existingSearches, englishTags);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -33,17 +43,14 @@ public class VoiceWorkSearchUpdater(AppDbContext dbContext) : IVoiceWorkSearchUp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.SaveChanges();
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSearchText(VoiceWork voiceWork)
|
private void UpdateSearchText(VoiceWork voiceWork, Dictionary<int, VoiceWorkSearch> existingSearches, Dictionary<int, EnglishTag> englishTags)
|
||||||
{
|
{
|
||||||
string searchText = GetSearchText(voiceWork);
|
string searchText = GetSearchText(voiceWork, englishTags);
|
||||||
|
|
||||||
var searchEntry = dbContext.VoiceWorkSearches
|
if (!existingSearches.TryGetValue(voiceWork.VoiceWorkId, out var searchEntry))
|
||||||
.FirstOrDefault(s => s.VoiceWorkId == voiceWork.VoiceWorkId);
|
|
||||||
|
|
||||||
if (searchEntry == null)
|
|
||||||
{
|
{
|
||||||
dbContext.VoiceWorkSearches.Add(new VoiceWorkSearch
|
dbContext.VoiceWorkSearches.Add(new VoiceWorkSearch
|
||||||
{
|
{
|
||||||
@@ -52,12 +59,15 @@ public class VoiceWorkSearchUpdater(AppDbContext dbContext) : IVoiceWorkSearchUp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
if (!string.Equals(searchEntry.SearchText, searchText, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
searchEntry.SearchText = searchText;
|
searchEntry.SearchText = searchText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string GetSearchText(VoiceWork voiceWork)
|
private string GetSearchText(VoiceWork voiceWork, Dictionary<int, EnglishTag> englishTags)
|
||||||
{
|
{
|
||||||
var english = voiceWork.EnglishVoiceWorks.FirstOrDefault();
|
var english = voiceWork.EnglishVoiceWorks.FirstOrDefault();
|
||||||
|
|
||||||
@@ -80,12 +90,8 @@ public class VoiceWorkSearchUpdater(AppDbContext dbContext) : IVoiceWorkSearchUp
|
|||||||
|
|
||||||
AppendRaw(sb, tag.Name);
|
AppendRaw(sb, tag.Name);
|
||||||
|
|
||||||
var englishTag = dbContext.EnglishTags.FirstOrDefault(et => et.TagId == tag.TagId);
|
if (englishTags.TryGetValue(tag.TagId, out var englishTag))
|
||||||
|
AppendRaw(sb, englishTag.Name);
|
||||||
if (englishTag is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
AppendRaw(sb, englishTag?.Name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var creator in voiceWork.Creators.Select(vwc => vwc.Creator))
|
foreach (var creator in voiceWork.Creators.Select(vwc => vwc.Creator))
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
|||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Include(v => v.Creators)
|
.Include(v => v.Creators)
|
||||||
.Include(v => v.Tags)
|
.Include(v => v.Tags)
|
||||||
|
.Include(v => v.EnglishVoiceWorks)
|
||||||
.Include(v => v.Localizations)
|
.Include(v => v.Localizations)
|
||||||
.Include(v => v.SupportedLanguages)
|
.Include(v => v.SupportedLanguages)
|
||||||
.ToDictionaryAsync(v => v.ProductId, cancellationToken),
|
.ToDictionaryAsync(v => v.ProductId, cancellationToken),
|
||||||
@@ -161,6 +162,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
|||||||
UpsertVoiceWorkCreators(ingest, upsertContext);
|
UpsertVoiceWorkCreators(ingest, upsertContext);
|
||||||
UpsertVoiceWorkSupportedLanguages(ingest, upsertContext);
|
UpsertVoiceWorkSupportedLanguages(ingest, upsertContext);
|
||||||
UpsertSeries(ingest, upsertContext);
|
UpsertSeries(ingest, upsertContext);
|
||||||
|
UpsertVoiceWorkLocalizations(ingest, upsertContext);
|
||||||
|
|
||||||
return dbContext.Entry(voiceWork).State switch
|
return dbContext.Entry(voiceWork).State switch
|
||||||
{
|
{
|
||||||
@@ -372,7 +374,9 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
|||||||
|
|
||||||
int position = 1;
|
int position = 1;
|
||||||
|
|
||||||
foreach (string tagName in ingest.Tags)
|
string[] distinctTagNames = [.. ingest.Tags.Distinct()];
|
||||||
|
|
||||||
|
foreach (string tagName in distinctTagNames)
|
||||||
{
|
{
|
||||||
Tag tag = upsertContext.Tags[tagName];
|
Tag tag = upsertContext.Tags[tagName];
|
||||||
|
|
||||||
@@ -399,7 +403,9 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
|||||||
|
|
||||||
int position = 1;
|
int position = 1;
|
||||||
|
|
||||||
foreach (string creatorName in ingest.Creators)
|
string[] distinctCreatorNames = [.. ingest.Creators.Distinct()];
|
||||||
|
|
||||||
|
foreach (string creatorName in distinctCreatorNames)
|
||||||
{
|
{
|
||||||
Creator creator = upsertContext.Creators[creatorName];
|
Creator creator = upsertContext.Creators[creatorName];
|
||||||
|
|
||||||
@@ -424,6 +430,9 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
|||||||
VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
|
VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
|
||||||
Dictionary<string, VoiceWorkSupportedLanguage> existingLanguageLinks = voiceWork.SupportedLanguages.ToDictionary(x => x.Language);
|
Dictionary<string, VoiceWorkSupportedLanguage> existingLanguageLinks = voiceWork.SupportedLanguages.ToDictionary(x => x.Language);
|
||||||
|
|
||||||
|
if (ingest.SupportedLanguages.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
foreach (SupportedLanguage supportedLanguage in ingest.SupportedLanguages)
|
foreach (SupportedLanguage supportedLanguage in ingest.SupportedLanguages)
|
||||||
{
|
{
|
||||||
if (!existingLanguageLinks.TryGetValue(supportedLanguage.Code, out VoiceWorkSupportedLanguage? voiceWorkSupportedLanguage))
|
if (!existingLanguageLinks.TryGetValue(supportedLanguage.Code, out VoiceWorkSupportedLanguage? voiceWorkSupportedLanguage))
|
||||||
@@ -437,6 +446,14 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
|||||||
dbContext.VoiceWorkSupportedLanguages.Add(voiceWorkSupportedLanguage);
|
dbContext.VoiceWorkSupportedLanguages.Add(voiceWorkSupportedLanguage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (string existingLinkCode in existingLanguageLinks.Keys)
|
||||||
|
{
|
||||||
|
if (!ingest.SupportedLanguages.Any(x => x.Code == existingLinkCode))
|
||||||
|
{
|
||||||
|
dbContext.VoiceWorkSupportedLanguages.Remove(existingLanguageLinks[existingLinkCode]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpsertSeries(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
|
private void UpsertSeries(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
|
||||||
@@ -473,4 +490,39 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
|
|||||||
|
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpsertVoiceWorkLocalizations(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
|
||||||
|
{
|
||||||
|
// For now, just adding/updating English voice works
|
||||||
|
foreach (VoiceWorkLocalizationIngest localizationIngest in ingest.Localizations)
|
||||||
|
{
|
||||||
|
if (localizationIngest.Language is Language.English)
|
||||||
|
{
|
||||||
|
EnglishVoiceWork englishVoiceWork = GetOrAddEnglishVoiceWork(ingest, upsertContext);
|
||||||
|
englishVoiceWork.ProductName = localizationIngest.Title ?? string.Empty;
|
||||||
|
englishVoiceWork.Description = localizationIngest.Description ?? string.Empty;
|
||||||
|
englishVoiceWork.IsValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnglishVoiceWork GetOrAddEnglishVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
|
||||||
|
{
|
||||||
|
VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
|
||||||
|
EnglishVoiceWork? englishVoiceWork = voiceWork.EnglishVoiceWorks.FirstOrDefault();
|
||||||
|
|
||||||
|
if (englishVoiceWork is null)
|
||||||
|
{
|
||||||
|
englishVoiceWork = new EnglishVoiceWork
|
||||||
|
{
|
||||||
|
VoiceWork = voiceWork,
|
||||||
|
ProductName = string.Empty,
|
||||||
|
Description = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.EnglishVoiceWorks.Add(englishVoiceWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
return englishVoiceWork;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using JSMR.Application.Integrations.Chobit.Models;
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.Chobit.Ports;
|
||||||
using JSMR.Infrastructure.Http;
|
using JSMR.Infrastructure.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using JSMR.Application.Integrations.DLSite.Models;
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
using JSMR.Infrastructure.Globalization;
|
||||||
using JSMR.Infrastructure.Http;
|
using JSMR.Infrastructure.Http;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Integrations.DLSite;
|
namespace JSMR.Infrastructure.Integrations.DLSite;
|
||||||
@@ -23,4 +26,23 @@ public class DLSiteClient(HttpClient http, ILogger<DLSiteClient> logger) : ApiCl
|
|||||||
|
|
||||||
return DLSiteToDomainMapper.Map(productInfoCollection);
|
return DLSiteToDomainMapper.Map(productInfoCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(ReleasedWorksRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
string locale = LocaleMapper.ToDLSiteLocale(request.Locale);
|
||||||
|
string date = request.Date.ToString("yyyy-MM-dd");
|
||||||
|
string url = $"maniax/new/work/api?locale={locale}&date={date}&period={request.Period}";
|
||||||
|
|
||||||
|
NewWorksApiResponse response = await GetJsonAsync<NewWorksApiResponse>(url, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
if (response.Meta.Code != 200)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"DLsite returned code {response.Meta.Code}. " +
|
||||||
|
$"ErrorType: {response.Meta.ErrorType}. " +
|
||||||
|
$"ErrorMessage: {response.Meta.ErrorMessage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return DLSiteReleasedWorksMapper.Map(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using JSMR.Application.Integrations.Ports;
|
//using JSMR.Application.Integrations.Ports;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Serialization;
|
//using JSMR.Infrastructure.Integrations.DLSite.Serialization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
//using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Integrations.DLSite;
|
//namespace JSMR.Infrastructure.Integrations.DLSite;
|
||||||
|
|
||||||
public static class DLSiteClientRegistration
|
public static class DLSiteClientRegistration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
||||||
|
|
||||||
|
public static class DLSiteReleasedWorksMapper
|
||||||
|
{
|
||||||
|
public static ReleasedWorksCollection Map(NewWorksApiResponse response)
|
||||||
|
{
|
||||||
|
ReleasedWorksCollection result = [];
|
||||||
|
|
||||||
|
if (response.Data.Products.Length == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
foreach (NewWorksApiProduct product in response.Data.Products)
|
||||||
|
{
|
||||||
|
result.Add(product.Id, Map(product));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReleasedWork Map(NewWorksApiProduct product)
|
||||||
|
{
|
||||||
|
return new ReleasedWork
|
||||||
|
{
|
||||||
|
ProductId = product.Id,
|
||||||
|
Title = product.Name ?? string.Empty,
|
||||||
|
MaskedTitle = product.NameMasked ?? string.Empty,
|
||||||
|
Description = product.Description ?? string.Empty,
|
||||||
|
MaskedDescription = product.DescriptionMasked ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("products")]
|
||||||
|
public required NewWorksApiProduct[] Products { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiMeta
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public int Code { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errorMessage")]
|
||||||
|
public string ErrorMessage { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("errorType")]
|
||||||
|
public string ErrorType { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiProduct
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public required string Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("nameMasked")]
|
||||||
|
public string? NameMasked { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("descriptionMasked")]
|
||||||
|
public string? DescriptionMasked { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
|
|
||||||
|
public record NewWorksApiResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("meta")]
|
||||||
|
public required NewWorksApiMeta Meta { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public required NewWorksApiData Data { get; init; }
|
||||||
|
}
|
||||||
@@ -15,16 +15,16 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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="HtmlAgilityPack" Version="1.12.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.4.0" />
|
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="NTextCat" Version="0.3.65" />
|
<PackageReference Include="NTextCat" Version="0.3.65" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
@@ -35,4 +35,8 @@
|
|||||||
<ProjectReference Include="..\JSMR.Domain\JSMR.Domain.csproj" />
|
<ProjectReference Include="..\JSMR.Domain\JSMR.Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Jobs\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using JSMR.Application.Enums;
|
using JSMR.Application.Enums;
|
||||||
using JSMR.Domain.ValueObjects;
|
using JSMR.Domain.ValueObjects;
|
||||||
|
using JSMR.Infrastructure.Globalization;
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Scanning;
|
namespace JSMR.Infrastructure.Scanning;
|
||||||
|
|
||||||
@@ -90,7 +91,10 @@ public sealed class DLSiteSearchFilterBuilder
|
|||||||
|
|
||||||
public string BuildSearchQuery(int pageNumber, int pageSize)
|
public string BuildSearchQuery(int pageNumber, int pageSize)
|
||||||
{
|
{
|
||||||
var (localeAbbreviation, localeCode) = LocaleMap.Map[_locale];
|
//string localeAbbreviation = LocaleMapper.ToAbbreviation(_locale);
|
||||||
|
//string localeCode = LocaleMapper.ToDLSiteLocale(_locale);
|
||||||
|
|
||||||
|
var (localeAbbreviation, localeCode) = LocaleMapper.Map[_locale];
|
||||||
|
|
||||||
using (var writer = new StringWriter())
|
using (var writer = new StringWriter())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using JSMR.Application.Enums;
|
|
||||||
|
|
||||||
namespace JSMR.Infrastructure.Scanning;
|
|
||||||
|
|
||||||
internal class LocaleMap
|
|
||||||
{
|
|
||||||
public static readonly IReadOnlyDictionary<Locale, (string Abbreviation, string Code)> Map =
|
|
||||||
new Dictionary<Locale, (string, string)>
|
|
||||||
{
|
|
||||||
{ Locale.Japanese, ("jp", "ja_JP") },
|
|
||||||
{ Locale.English, ("en", "en_US") },
|
|
||||||
{ Locale.ChineseSimplified, ("zh-cn", "zh_CN") },
|
|
||||||
{ Locale.ChineseTraditional, ("zh-tw", "zh_TW") },
|
|
||||||
{ Locale.Korean, ("ko", "ko_KR") },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
93
JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs
Normal file
93
JSMR.Infrastructure/Scanning/ReleasedWorksProvider.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using JSMR.Application.Enums;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
using JSMR.Application.Scanning.Contracts;
|
||||||
|
using JSMR.Application.Scanning.Ports;
|
||||||
|
|
||||||
|
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 =
|
||||||
|
[
|
||||||
|
.. scanResult.Works
|
||||||
|
.Where(x => x.SalesDate.HasValue)
|
||||||
|
.Select(x => x.SalesDate!.Value)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (salesDates.Length == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
HashSet<string> productIds = [.. scanResult.Works.Select(x => x.ProductId)];
|
||||||
|
|
||||||
|
DateOnly minDate = salesDates.Min();
|
||||||
|
DateOnly maxDate = salesDates.Max();
|
||||||
|
|
||||||
|
ReleasedWorksCollection collection = [];
|
||||||
|
|
||||||
|
DateOnly chunkStart = minDate;
|
||||||
|
|
||||||
|
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: chunkEnd,
|
||||||
|
Period: period);
|
||||||
|
|
||||||
|
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);
|
||||||
|
//}
|
||||||
|
}
|
||||||
94
JSMR.Infrastructure/Scanning/VoiceWorkIngestBuilder.cs
Normal file
94
JSMR.Infrastructure/Scanning/VoiceWorkIngestBuilder.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
|
using JSMR.Application.Integrations.Chobit.Ports;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
|
using JSMR.Application.Scanning.Contracts;
|
||||||
|
using JSMR.Application.Scanning.Ports;
|
||||||
|
using JSMR.Domain.Enums;
|
||||||
|
using JSMR.Infrastructure.Common.Languages;
|
||||||
|
|
||||||
|
namespace JSMR.Infrastructure.Scanning;
|
||||||
|
|
||||||
|
public class VoiceWorkIngestBuilder(
|
||||||
|
IDLSiteClient dlsiteClient,
|
||||||
|
IChobitClient chobitClient,
|
||||||
|
IReleasedWorksProvider releasedWorksProvider,
|
||||||
|
ILanguageIdentifier languageIdentifier) : IVoiceWorkIngestBuilder
|
||||||
|
{
|
||||||
|
public async Task<VoiceWorkIngest[]> BuildAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
|
||||||
|
|
||||||
|
Task<VoiceWorkDetailCollection> detailsTask = dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
|
||||||
|
Task<ChobitResultCollection> chobitTask = chobitClient.GetSampleInfoAsync(productIds, cancellationToken);
|
||||||
|
Task<ReleasedWorksCollection> releasedTask = releasedWorksProvider.GetReleasedWorksAsync(scanResult, cancellationToken);
|
||||||
|
|
||||||
|
await Task.WhenAll(detailsTask, chobitTask, releasedTask);
|
||||||
|
|
||||||
|
VoiceWorkDetailCollection voiceWorkDetails = await detailsTask;
|
||||||
|
ChobitResultCollection chobitResults = await chobitTask;
|
||||||
|
ReleasedWorksCollection releasedWorkCollection = await releasedTask;
|
||||||
|
|
||||||
|
List<VoiceWorkIngest> ingests = [];
|
||||||
|
|
||||||
|
foreach (DLSiteWork work in scanResult.Works)
|
||||||
|
{
|
||||||
|
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? details);
|
||||||
|
chobitResults.TryGetValue(work.ProductId, out ChobitResult? chobit);
|
||||||
|
releasedWorkCollection.TryGetValue(work.ProductId, out ReleasedWork? releasedWork);
|
||||||
|
|
||||||
|
VoiceWorkIngest ingest = new()
|
||||||
|
{
|
||||||
|
MakerId = work.MakerId,
|
||||||
|
MakerName = work.Maker,
|
||||||
|
ProductId = work.ProductId,
|
||||||
|
Title = details?.Title ?? work.ProductName,
|
||||||
|
Description = work.Description ?? string.Empty,
|
||||||
|
Tags = work.Tags,
|
||||||
|
Creators = work.Creators,
|
||||||
|
WishlistCount = details?.WishlistCount ?? 0,
|
||||||
|
Downloads = Math.Max(work.Downloads, details?.DownloadCount ?? 0),
|
||||||
|
HasTrial = work.HasTrial || (details?.HasTrial ?? false),
|
||||||
|
HasChobit = chobit?.Count > 0,
|
||||||
|
StarRating = work.StarRating,
|
||||||
|
Votes = work.Votes,
|
||||||
|
AgeRating = details?.AgeRating ?? work.AgeRating,
|
||||||
|
HasImage = !string.IsNullOrEmpty(work.ImageUrl) && !work.ImageUrl.Contains("no_img", StringComparison.OrdinalIgnoreCase),
|
||||||
|
SupportedLanguages = details?.SupportedLanguages ?? [],
|
||||||
|
ExpectedDate = work.ExpectedDate,
|
||||||
|
SalesDate = work.SalesDate,
|
||||||
|
RegistrationDate = details?.RegistrationDate,
|
||||||
|
AI = details?.AI ?? AIGeneration.None,
|
||||||
|
Series = details?.Series,
|
||||||
|
Translation = details?.Translation,
|
||||||
|
Localizations = GetLocalizationIngests(releasedWork)
|
||||||
|
};
|
||||||
|
|
||||||
|
ingests.Add(ingest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. ingests];
|
||||||
|
}
|
||||||
|
|
||||||
|
private VoiceWorkLocalizationIngest[] GetLocalizationIngests(ReleasedWork? releasedWork)
|
||||||
|
{
|
||||||
|
if (releasedWork is null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
Language titleLanguage = languageIdentifier.GetLanguage(releasedWork.Title);
|
||||||
|
Language descriptionLanguage = languageIdentifier.GetLanguage(releasedWork.Description);
|
||||||
|
|
||||||
|
if (titleLanguage is not Language.English && descriptionLanguage is not Language.English)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
VoiceWorkLocalizationIngest localizationIngest = new()
|
||||||
|
{
|
||||||
|
Title = releasedWork.Title,
|
||||||
|
Description = releasedWork.Description,
|
||||||
|
Language = Language.English
|
||||||
|
};
|
||||||
|
|
||||||
|
return [localizationIngest];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using JSMR.Infrastructure.Data;
|
||||||
|
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||||
|
using JSMR.Tests.Fixtures;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace JSMR.Tests.Data.Repositories.VoiceWorks;
|
||||||
|
|
||||||
|
public class Set_Is_Favorite_Tests(MariaDbContainerFixture container) : VoiceWorkRepositoryTests(container)
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Set_Is_Favorite()
|
||||||
|
{
|
||||||
|
await using AppDbContext dbContext = await GetAppDbContextAsync();
|
||||||
|
VoiceWorkWriter writer = new(dbContext);
|
||||||
|
|
||||||
|
await SetFavoriteAndVerifyAsync(dbContext, writer, 1, true);
|
||||||
|
await SetFavoriteAndVerifyAsync(dbContext, writer, 1, true);
|
||||||
|
await SetFavoriteAndVerifyAsync(dbContext, writer, 1, false);
|
||||||
|
await SetFavoriteAndVerifyAsync(dbContext, writer, 1, false);
|
||||||
|
await SetFavoriteAndVerifyAsync(dbContext, writer, 1, true);
|
||||||
|
await SetFavoriteAndVerifyAsync(dbContext, writer, 1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SetFavoriteAndVerifyAsync(AppDbContext dbContext, VoiceWorkWriter writer, int voiceWorkId, bool isFavorite)
|
||||||
|
{
|
||||||
|
SetVoiceWorkFavoriteRequest request = new(
|
||||||
|
VoiceWorkId: voiceWorkId,
|
||||||
|
IsFavorite: isFavorite
|
||||||
|
);
|
||||||
|
|
||||||
|
SetVoiceWorkFavoriteResponse response = await writer.SetFavoriteAsync(request, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
response.VoiceWorkId.ShouldBe(voiceWorkId);
|
||||||
|
response.IsFavorite.ShouldBe(isFavorite);
|
||||||
|
|
||||||
|
VoiceWork? voiceWork = await dbContext.VoiceWorks
|
||||||
|
.SingleAsync(v => v.VoiceWorkId == voiceWorkId, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
voiceWork.ShouldNotBeNull();
|
||||||
|
voiceWork.Favorite.ShouldBe(isFavorite);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using JSMR.Domain.Enums;
|
||||||
|
using JSMR.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JSMR.Tests.Data.Repositories.VoiceWorks;
|
||||||
|
|
||||||
|
public static class VoiceWorkRepositorySeedData
|
||||||
|
{
|
||||||
|
public static async Task SeedAsync(AppDbContext context)
|
||||||
|
{
|
||||||
|
if (await context.VoiceWorks.AnyAsync())
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.Circles.AddRange(
|
||||||
|
new() { CircleId = 1, Name = "Good Dreams", MakerId = "RG00001" },
|
||||||
|
new() { CircleId = 2, Name = "Sweet Dreams", Favorite = true, MakerId = "RG00002" },
|
||||||
|
new() { CircleId = 3, Name = "Nightmare Fuel", Blacklisted = true, MakerId = "RG00003" },
|
||||||
|
new() { CircleId = 4, Name = "Never Again", Spam = true, MakerId = "RG00004" }
|
||||||
|
);
|
||||||
|
|
||||||
|
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, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using JSMR.Infrastructure.Data;
|
||||||
|
using JSMR.Tests.Fixtures;
|
||||||
|
|
||||||
|
namespace JSMR.Tests.Data.Repositories.VoiceWorks;
|
||||||
|
|
||||||
|
public abstract class VoiceWorkRepositoryTests(MariaDbContainerFixture container)
|
||||||
|
{
|
||||||
|
protected async Task<AppDbContext> GetAppDbContextAsync()
|
||||||
|
{
|
||||||
|
return await MariaDbClone.CloneFromTemplateAsync(
|
||||||
|
container.RootConnectionString,
|
||||||
|
container.TemplateDbName,
|
||||||
|
seed: VoiceWorkRepositorySeedData.SeedAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,60 @@
|
|||||||
namespace JSMR.Tests.Http;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace JSMR.Tests.Http;
|
||||||
|
|
||||||
internal sealed class FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
|
internal sealed class FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
|
||||||
{
|
{
|
||||||
|
public List<HttpRequestMessage> Requests { get; } = [];
|
||||||
|
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Requests.Add(request);
|
||||||
|
|
||||||
return Task.FromResult(handler(request));
|
return Task.FromResult(handler(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class HttpRequestMessageAssertions
|
||||||
|
{
|
||||||
|
public static void ShouldHavePath(this HttpRequestMessage request, string expectedPath)
|
||||||
|
{
|
||||||
|
request.RequestUri.ShouldNotBeNull();
|
||||||
|
request.RequestUri!.AbsolutePath.ShouldBe(expectedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ShouldHaveQueryParam(this HttpRequestMessage request, string key, string expectedValue)
|
||||||
|
{
|
||||||
|
request.RequestUri.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var query = QueryHelpers.ParseQuery(request.RequestUri!.Query);
|
||||||
|
query[key].ToString().ShouldBe(expectedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//internal sealed class FakeHttpMessageHandler : HttpMessageHandler
|
||||||
|
//{
|
||||||
|
// private readonly Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> _handler;
|
||||||
|
|
||||||
|
// public List<HttpRequestMessage> Requests { get; } = [];
|
||||||
|
|
||||||
|
// public FakeHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> handler)
|
||||||
|
// {
|
||||||
|
// _handler = handler;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
|
||||||
|
// : this((request, _) => handler(request))
|
||||||
|
// {
|
||||||
|
// }
|
||||||
|
|
||||||
|
// protected override Task<HttpResponseMessage> SendAsync(
|
||||||
|
// HttpRequestMessage request,
|
||||||
|
// CancellationToken cancellationToken)
|
||||||
|
// {
|
||||||
|
// Requests.Add(request);
|
||||||
|
|
||||||
|
// HttpResponseMessage response = _handler(request, cancellationToken);
|
||||||
|
// return Task.FromResult(response);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using JSMR.Application.Scanning.Contracts;
|
||||||
|
using JSMR.Domain.Entities;
|
||||||
|
using JSMR.Domain.ValueObjects;
|
||||||
|
using JSMR.Infrastructure.Data;
|
||||||
|
using JSMR.Tests.Fixtures;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace JSMR.Tests.Ingestion.Japanese;
|
||||||
|
|
||||||
|
public class Update_Supported_Langauge_Tests(MariaDbContainerFixture container) : JapaneseIngestionTestsBase(container)
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Update_Supported_Langauge()
|
||||||
|
{
|
||||||
|
VoiceWorkIngest ingest = new()
|
||||||
|
{
|
||||||
|
MakerId = "RG1",
|
||||||
|
MakerName = "The Maker",
|
||||||
|
ProductId = "RJ1",
|
||||||
|
Title = "Title",
|
||||||
|
Description = "Description",
|
||||||
|
SupportedLanguages = [SupportedLanguage.Japanese]
|
||||||
|
};
|
||||||
|
|
||||||
|
await using AppDbContext dbContext = await GetAppDbContextAsync();
|
||||||
|
DateTime currentDateTime = TokyoLocalToUtc(2025, 01, 05, 10, 0, 0);
|
||||||
|
|
||||||
|
await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 05, 10, 0, 0), ingest, [SupportedLanguage.Japanese]);
|
||||||
|
|
||||||
|
// Add English
|
||||||
|
VoiceWorkIngest addSupportedLanguageIngest = ingest with
|
||||||
|
{
|
||||||
|
SupportedLanguages = [SupportedLanguage.Japanese, SupportedLanguage.English]
|
||||||
|
};
|
||||||
|
|
||||||
|
await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 05, 10, 0, 0), addSupportedLanguageIngest, [SupportedLanguage.Japanese, SupportedLanguage.English]);
|
||||||
|
|
||||||
|
// Remove Japanese
|
||||||
|
VoiceWorkIngest removeSupportedLanguageIngest = ingest with
|
||||||
|
{
|
||||||
|
SupportedLanguages = [SupportedLanguage.English]
|
||||||
|
};
|
||||||
|
|
||||||
|
await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 05, 10, 0, 0), removeSupportedLanguageIngest, [SupportedLanguage.English]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task UpsertAndVerify(AppDbContext dbContext, DateTime dateTime, VoiceWorkIngest ingest, SupportedLanguage[] expectedSupportedLanguages)
|
||||||
|
{
|
||||||
|
await UpsertAsync(dbContext, dateTime, [ingest]);
|
||||||
|
|
||||||
|
VoiceWork? voiceWork = await dbContext.VoiceWorks
|
||||||
|
.Include(x => x.SupportedLanguages)
|
||||||
|
.SingleAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
voiceWork.ShouldNotBeNull();
|
||||||
|
|
||||||
|
string[] languageCodes = [.. voiceWork.SupportedLanguages.Select(x => x.Language).OrderBy(x => x)];
|
||||||
|
string[] expectedLanguageCode = [.. expectedSupportedLanguages.Select(x => x.Code).OrderBy(x => x)];
|
||||||
|
|
||||||
|
languageCodes.ShouldBe(expectedLanguageCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ public class Update_Upcoming_With_No_Expected_Date_Tests(MariaDbContainerFixture
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Should be exactly the same
|
// Should be exactly the same
|
||||||
await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 05, 10, 0, 0), ingest, new DateTime(2025, 1, 21));
|
await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 05, 10, 0, 0), updatedIngest, new DateTime(2025, 1, 21));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task UpsertAndVerify(AppDbContext dbContext, DateTime dateTime, VoiceWorkIngest ingest, DateTime? expectedDate)
|
private static async Task UpsertAndVerify(AppDbContext dbContext, DateTime dateTime, VoiceWorkIngest ingest, DateTime? expectedDate)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using JSMR.Application.Integrations.DLSite.Models;
|
using JSMR.Application.Enums;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||||
using JSMR.Domain.Enums;
|
using JSMR.Domain.Enums;
|
||||||
using JSMR.Domain.ValueObjects;
|
using JSMR.Domain.ValueObjects;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite;
|
using JSMR.Infrastructure.Integrations.DLSite;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
using JSMR.Infrastructure.Integrations.DLSite.Mapping;
|
||||||
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
using JSMR.Infrastructure.Integrations.DLSite.Models;
|
||||||
|
using JSMR.Infrastructure.Integrations.DLSite.Models.NewWorks;
|
||||||
using JSMR.Tests.Http;
|
using JSMR.Tests.Http;
|
||||||
using JSMR.Tests.Utilities;
|
using JSMR.Tests.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -21,11 +24,11 @@ public class DLSiteClientTests
|
|||||||
return await ResourceHelper.ReadAsync($"JSMR.Tests.Integrations.DLSite.{resourceName}");
|
return await ResourceHelper.ReadAsync($"JSMR.Tests.Integrations.DLSite.{resourceName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DLSiteClient> GetDLSiteClientThatReturns(string resourceName)
|
private static async Task<(DLSiteClient Client, FakeHttpMessageHandler Handler)> GetDLSiteClientThatReturns(string resourceName)
|
||||||
{
|
{
|
||||||
string content = await ReadJsonResourceAsync(resourceName);
|
string content = await ReadJsonResourceAsync(resourceName);
|
||||||
|
|
||||||
FakeHttpMessageHandler handler = new(request =>
|
FakeHttpMessageHandler handler = new(_ =>
|
||||||
{
|
{
|
||||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
@@ -41,13 +44,27 @@ public class DLSiteClientTests
|
|||||||
var logger = Substitute.For<ILogger<DLSiteClient>>();
|
var logger = Substitute.For<ILogger<DLSiteClient>>();
|
||||||
var client = new DLSiteClient(httpClient, logger);
|
var client = new DLSiteClient(httpClient, logger);
|
||||||
|
|
||||||
return client;
|
return (client, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetVoiceWorkDetailsAsync_Should_Call_Expected_Request()
|
||||||
|
{
|
||||||
|
var (client, handler) = await GetDLSiteClientThatReturns("Product-Info.json");
|
||||||
|
await client.GetVoiceWorkDetailsAsync(["RJ01230163", "RJ01536422"], CancellationToken.None);
|
||||||
|
|
||||||
|
handler.Requests.Count.ShouldBe(1);
|
||||||
|
|
||||||
|
HttpRequestMessage httpRequest = handler.Requests[0];
|
||||||
|
httpRequest.Method.ShouldBe(HttpMethod.Get);
|
||||||
|
httpRequest.ShouldHavePath("/maniax/product/info/ajax");
|
||||||
|
httpRequest.ShouldHaveQueryParam("product_id", "RJ01230163,RJ01536422");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Deserialize_Product_Info_Collection()
|
public async Task Deserialize_Product_Info_Collection()
|
||||||
{
|
{
|
||||||
var client = await GetDLSiteClientThatReturns("Product-Info.json");
|
var (client, handler) = await GetDLSiteClientThatReturns("Product-Info.json");
|
||||||
var result = await client.GetVoiceWorkDetailsAsync(["RJ01230163", "RJ01536422"], CancellationToken.None);
|
var result = await client.GetVoiceWorkDetailsAsync(["RJ01230163", "RJ01536422"], CancellationToken.None);
|
||||||
|
|
||||||
result.Count.ShouldBe(2);
|
result.Count.ShouldBe(2);
|
||||||
@@ -144,4 +161,78 @@ public class DLSiteClientTests
|
|||||||
secondVoiceWorkDetails.Translation.Language.ShouldBe(Language.English);
|
secondVoiceWorkDetails.Translation.Language.ShouldBe(Language.English);
|
||||||
secondVoiceWorkDetails.Translation.Kind.ShouldBe(TranslationKind.Official | TranslationKind.Recommended);
|
secondVoiceWorkDetails.Translation.Kind.ShouldBe(TranslationKind.Official | TranslationKind.Recommended);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetReleasedWorksAsync_Should_Call_Expected_Request()
|
||||||
|
{
|
||||||
|
var (client, handler) = await GetDLSiteClientThatReturns("Released-Works.json");
|
||||||
|
|
||||||
|
ReleasedWorksRequest request = new(Locale.English, new(2025, 1, 1), 1);
|
||||||
|
|
||||||
|
await client.GetReleasedWorksAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
handler.Requests.Count.ShouldBe(1);
|
||||||
|
|
||||||
|
HttpRequestMessage httpRequest = handler.Requests[0];
|
||||||
|
httpRequest.Method.ShouldBe(HttpMethod.Get);
|
||||||
|
httpRequest.ShouldHavePath("/maniax/new/work/api");
|
||||||
|
httpRequest.ShouldHaveQueryParam("locale", "en-us");
|
||||||
|
httpRequest.ShouldHaveQueryParam("date", "2025-01-01");
|
||||||
|
httpRequest.ShouldHaveQueryParam("period", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Map_Basic_Released_Work_Collection()
|
||||||
|
{
|
||||||
|
NewWorksApiResponse response = new()
|
||||||
|
{
|
||||||
|
Meta = new()
|
||||||
|
{
|
||||||
|
Code = 200
|
||||||
|
},
|
||||||
|
Data = new()
|
||||||
|
{
|
||||||
|
Products =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "RG1",
|
||||||
|
Name = "The Title",
|
||||||
|
NameMasked = "Masked Title",
|
||||||
|
Description = "The description",
|
||||||
|
DescriptionMasked = "The masked description"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ReleasedWorksCollection collection = DLSiteReleasedWorksMapper.Map(response);
|
||||||
|
collection.Count.ShouldBe(1);
|
||||||
|
collection.ShouldContainKey("RG1");
|
||||||
|
|
||||||
|
ReleasedWork releasedWork = collection["RG1"];
|
||||||
|
releasedWork.ProductId.ShouldBe("RG1");
|
||||||
|
releasedWork.Title.ShouldBe("The Title");
|
||||||
|
releasedWork.MaskedTitle.ShouldBe("Masked Title");
|
||||||
|
releasedWork.Description.ShouldBe("The description");
|
||||||
|
releasedWork.MaskedDescription.ShouldBe("The masked description");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Deserialize_Released_Work_Collection()
|
||||||
|
{
|
||||||
|
var (client, handler) = await GetDLSiteClientThatReturns("Released-Works.json");
|
||||||
|
var request = new ReleasedWorksRequest(Locale.English, new(2025, 1, 1), 1);
|
||||||
|
var result = await client.GetReleasedWorksAsync(request, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Count.ShouldBe(13);
|
||||||
|
|
||||||
|
result.ShouldContainKey("RJ01588345");
|
||||||
|
|
||||||
|
ReleasedWork releasedWork = result["RJ01588345"];
|
||||||
|
releasedWork.Title.ShouldBe("魔都一贅肉のスゴイデブ");
|
||||||
|
releasedWork.MaskedTitle.ShouldBe("魔都一贅肉のスゴイデブ");
|
||||||
|
releasedWork.Description.ShouldBe("圧巻の超連続・激太りご褒美パラダイス、ここに!全24ページ描き下ろし!太→激太への肥満化シークエンスが11作収録!餌付け、自己肥育、お風呂、縛り…などなど多種多様の太り方でご褒美を堪能せよ!");
|
||||||
|
releasedWork.MaskedDescription.ShouldBe("圧巻の超連続・激太りご褒美パラダイス、ここに!全24ページ描き下ろし!太→激太への肥満化シークエンスが11作収録!餌付け、自己肥育、お風呂、縛り…などなど多種多様の太り方でご褒美を堪能せよ!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
4922
JSMR.Tests/Integrations/DLSite/Released-Works.json
Normal file
4922
JSMR.Tests/Integrations/DLSite/Released-Works.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
|||||||
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-No-Data.jsonp" />
|
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-No-Data.jsonp" />
|
||||||
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-Collection.jsonp" />
|
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-Collection.jsonp" />
|
||||||
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result.jsonp" />
|
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result.jsonp" />
|
||||||
|
<EmbeddedResource Include="Integrations\DLSite\Released-Works.json" />
|
||||||
<EmbeddedResource Include="Integrations\DLSite\Product-Info.json" />
|
<EmbeddedResource Include="Integrations\DLSite\Product-Info.json" />
|
||||||
<EmbeddedResource Include="Scanning\English-Page-Updated.html" />
|
<EmbeddedResource Include="Scanning\English-Page-Updated.html" />
|
||||||
<EmbeddedResource Include="Scanning\Japanese-Page-Updated.html" />
|
<EmbeddedResource Include="Scanning\Japanese-Page-Updated.html" />
|
||||||
@@ -25,17 +26,18 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="8.0.0">
|
<PackageReference Include="coverlet.collector" Version="10.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.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="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="Testcontainers" Version="4.10.0" />
|
<PackageReference Include="Testcontainers" Version="4.11.0" />
|
||||||
<PackageReference Include="Testcontainers.MariaDb" Version="4.10.0" />
|
<PackageReference Include="Testcontainers.MariaDb" Version="4.11.0" />
|
||||||
<PackageReference Include="Testcontainers.XunitV3" Version="4.10.0" />
|
<PackageReference Include="Testcontainers.XunitV3" Version="4.11.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using JSMR.Application.Integrations.Chobit.Models;
|
using JSMR.Application.Integrations.Chobit.Models;
|
||||||
using JSMR.Application.Integrations.DLSite.Models;
|
using JSMR.Application.Integrations.DLSite.Models;
|
||||||
using JSMR.Application.Integrations.Ports;
|
using JSMR.Application.Integrations.DLSite.Ports;
|
||||||
using JSMR.Application.Scanning.Contracts;
|
using JSMR.Application.Scanning.Contracts;
|
||||||
using JSMR.Application.Scanning.Ports;
|
using JSMR.Application.Scanning.Ports;
|
||||||
using JSMR.Infrastructure.Http;
|
using JSMR.Infrastructure.Http;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public class CreatorSearchProviderTests(CreatorSearchProviderFixture fixture) :
|
|||||||
{
|
{
|
||||||
var options = new SearchOptions<CreatorSearchCriteria, CreatorSortField>()
|
var options = new SearchOptions<CreatorSearchCriteria, CreatorSortField>()
|
||||||
{
|
{
|
||||||
SortOptions = [new(CreatorSortField.Favorite, SortDirection.Ascending)]
|
SortOptions = [new(CreatorSortField.Favorite, SortDirection.Descending)]
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await SearchAsync(options);
|
var result = await SearchAsync(options);
|
||||||
@@ -67,7 +67,7 @@ public class CreatorSearchProviderTests(CreatorSearchProviderFixture fixture) :
|
|||||||
{
|
{
|
||||||
var options = new SearchOptions<CreatorSearchCriteria, CreatorSortField>()
|
var options = new SearchOptions<CreatorSearchCriteria, CreatorSortField>()
|
||||||
{
|
{
|
||||||
SortOptions = [new(CreatorSortField.Blacklisted, SortDirection.Ascending)]
|
SortOptions = [new(CreatorSortField.Blacklisted, SortDirection.Descending)]
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await SearchAsync(options);
|
var result = await SearchAsync(options);
|
||||||
|
|||||||
@@ -84,4 +84,30 @@ public class TagSearchProviderTests(TagSearchProviderFixture fixture) : IClassFi
|
|||||||
result.TotalItems.ShouldBe(1);
|
result.TotalItems.ShouldBe(1);
|
||||||
result.Items.ShouldContain(tagView => tagView.EnglishName == "Heartwarming");
|
result.Items.ShouldContain(tagView => tagView.EnglishName == "Heartwarming");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Filter_None_Sort_Favorite_Descending()
|
||||||
|
{
|
||||||
|
var options = new SearchOptions<TagSearchCriteria, TagSortField>()
|
||||||
|
{
|
||||||
|
SortOptions = [new(TagSortField.Favorite, SortDirection.Descending)]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await SearchAsync(options);
|
||||||
|
|
||||||
|
result.Items[0].EnglishName.ShouldBe("Heartwarming");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Filter_None_Sort_Blacklisted_Descending()
|
||||||
|
{
|
||||||
|
var options = new SearchOptions<TagSearchCriteria, TagSortField>()
|
||||||
|
{
|
||||||
|
SortOptions = [new(TagSortField.Blacklisted, SortDirection.Descending)]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await SearchAsync(options);
|
||||||
|
|
||||||
|
result.Items[0].EnglishName.ShouldBe("Tsundere");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using JSMR.Domain.Enums;
|
using JSMR.Domain.Entities;
|
||||||
|
using JSMR.Domain.Enums;
|
||||||
using JSMR.Infrastructure.Data;
|
using JSMR.Infrastructure.Data;
|
||||||
using JSMR.Tests.Fixtures;
|
using JSMR.Tests.Fixtures;
|
||||||
|
|
||||||
@@ -22,6 +23,14 @@ public sealed class VoiceWorkSearchProviderFixture(MariaDbContainerFixture conta
|
|||||||
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), PlannedReleaseDate = new(2025, 1, 13), WishlistCount = 10000, SubtitleLanguage = (byte)Language.English, Rating = (byte)AgeRating.R15, AIGeneration = (byte)AIGeneration.None }
|
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), PlannedReleaseDate = new(2025, 1, 13), WishlistCount = 10000, SubtitleLanguage = (byte)Language.English, Rating = (byte)AgeRating.R15, AIGeneration = (byte)AIGeneration.None }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
context.VoiceWorkSupportedLanguages.AddRange(
|
||||||
|
new() { VoiceWorkSupportedLanguageId = 1, VoiceWorkId = 1, Language = "JPN" },
|
||||||
|
new() { VoiceWorkSupportedLanguageId = 2, VoiceWorkId = 2, Language = "JPN" },
|
||||||
|
new() { VoiceWorkSupportedLanguageId = 3, VoiceWorkId = 3, Language = "JPN" },
|
||||||
|
new() { VoiceWorkSupportedLanguageId = 4, VoiceWorkId = 4, Language = "JPN" },
|
||||||
|
new() { VoiceWorkSupportedLanguageId = 5, VoiceWorkId = 5, Language = "ENG" }
|
||||||
|
);
|
||||||
|
|
||||||
context.Tags.AddRange(
|
context.Tags.AddRange(
|
||||||
new() { TagId = 1, Name = "ASMR" },
|
new() { TagId = 1, Name = "ASMR" },
|
||||||
new() { TagId = 2, Name = "OL" },
|
new() { TagId = 2, Name = "OL" },
|
||||||
|
|||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,18 @@
|
|||||||
@if (string.IsNullOrWhiteSpace(Url))
|
@if (string.IsNullOrWhiteSpace(Url))
|
||||||
{
|
{
|
||||||
<div class="@GetClasses()" @onclick="@OnClickAsync">
|
<div class="@GetClasses()" @onclick="@OnClickAsync">
|
||||||
@if (Graphic != null)
|
@if (Graphic != null && Graphic != Enums.Graphic.None)
|
||||||
{
|
{
|
||||||
<Icon Graphic="@Graphic.Value"
|
<Icon Graphic="@Graphic.Value"
|
||||||
Varient="@(IconVarient ?? Enums.IconVarient.None)"
|
Varient="@(IconVarient ?? Enums.IconVarient.None)"
|
||||||
Size="@(IconSize ?? Enums.SizeVarient.Small)"
|
Size="@(IconSize ?? Enums.SizeVarient.Small)"
|
||||||
Color="@Color">
|
UseCurrentColor>
|
||||||
</Icon>
|
</Icon>
|
||||||
}
|
}
|
||||||
|
@if (ChildContent is not null)
|
||||||
|
{
|
||||||
<span>@ChildContent</span>
|
<span>@ChildContent</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -23,10 +26,13 @@ else
|
|||||||
Graphic="@Graphic.Value"
|
Graphic="@Graphic.Value"
|
||||||
Varient="@(IconVarient ?? Enums.IconVarient.None)"
|
Varient="@(IconVarient ?? Enums.IconVarient.None)"
|
||||||
Size="@(IconSize ?? Enums.SizeVarient.Small)"
|
Size="@(IconSize ?? Enums.SizeVarient.Small)"
|
||||||
Color="@Color">
|
UseCurrentColor>
|
||||||
</Icon>
|
</Icon>
|
||||||
}
|
}
|
||||||
|
@if (ChildContent is not null)
|
||||||
|
{
|
||||||
<span>@ChildContent</span>
|
<span>@ChildContent</span>
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +68,12 @@ else
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback Click { get; set; }
|
public EventCallback Click { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsClickable { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ThickBorder { get; set; }
|
||||||
|
|
||||||
private string GetClasses()
|
private string GetClasses()
|
||||||
{
|
{
|
||||||
string color = Color.ToString().ToLower();
|
string color = Color.ToString().ToLower();
|
||||||
@@ -72,6 +84,17 @@ else
|
|||||||
$"color-{color}"
|
$"color-{color}"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Experimental
|
||||||
|
if (ChildContent is null)
|
||||||
|
{
|
||||||
|
classNames.Add("j-chip-icon-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ThickBorder)
|
||||||
|
{
|
||||||
|
classNames.Add("j-chip-thick-border");
|
||||||
|
}
|
||||||
|
|
||||||
switch (Varient)
|
switch (Varient)
|
||||||
{
|
{
|
||||||
case ElementVarient.Filled:
|
case ElementVarient.Filled:
|
||||||
@@ -94,7 +117,12 @@ else
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Click.HasDelegate || string.IsNullOrWhiteSpace(Url) == false)
|
if (Varient == ElementVarient.Filled && Tone == ToneVarient.Tint)
|
||||||
|
{
|
||||||
|
classNames.Add($"varient-tint");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Click.HasDelegate || string.IsNullOrWhiteSpace(Url) == false || IsClickable)
|
||||||
{
|
{
|
||||||
classNames.Add("is-clickable");
|
classNames.Add("is-clickable");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
if (Tag.IsBlacklisted)
|
if (Tag.IsBlacklisted)
|
||||||
{
|
{
|
||||||
return ColorVarient.Pink;
|
return ColorVarient.Red;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ColorVarient.Secondary;
|
return ColorVarient.Secondary;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@using JSMR.UI.Blazor.Enums
|
@using JSMR.UI.Blazor.Enums
|
||||||
|
|
||||||
<div class="@GetIconClasses()"></div>
|
<div class="@GetIconClasses()" style="@GetStyle()"></div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public ColorVarient Color { get; set; }
|
public ColorVarient Color { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool UseCurrentColor { get; set; }
|
||||||
|
|
||||||
private string GetIconClasses()
|
private string GetIconClasses()
|
||||||
{
|
{
|
||||||
string graphic = Varient == IconVarient.None
|
string graphic = Varient == IconVarient.None
|
||||||
@@ -26,9 +29,19 @@
|
|||||||
$"j-icon",
|
$"j-icon",
|
||||||
$"j-icon-{graphic}",
|
$"j-icon-{graphic}",
|
||||||
$"size-{Size.ToString().ToLower()}",
|
$"size-{Size.ToString().ToLower()}",
|
||||||
$"background-color-{Color.ToString().ToLower()}"
|
//$"background-color-{Color.ToString().ToLower()}"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!UseCurrentColor)
|
||||||
|
{
|
||||||
|
classNames.Add($"background-color-{Color.ToString().ToLower()}");
|
||||||
|
}
|
||||||
|
|
||||||
return string.Join(" ", classNames);
|
return string.Join(" ", classNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? GetStyle()
|
||||||
|
{
|
||||||
|
return UseCurrentColor ? "background-color: currentColor;" : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
113
JSMR.UI.Blazor/Components/JPagination2.razor
Normal file
113
JSMR.UI.Blazor/Components/JPagination2.razor
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
@using JSMR.UI.Blazor.Enums
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<div>
|
||||||
|
<label>@IndexInfo</label>
|
||||||
|
</div>
|
||||||
|
@* <MudPagination class="pager" ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)TotalItems / (decimal)PageSize))" Selected="@PageNumber" SelectedChanged="OnSelectedChanged" /> *@
|
||||||
|
<AntDesign.Pagination ShowSizeChanger="false" Total="TotalItems" Current="PageNumber" PageSize="PageSize" OnChange="OnPaginationChanged"></AntDesign.Pagination>
|
||||||
|
<div class="page-sizes">
|
||||||
|
@RightContent
|
||||||
|
<div class="page-size-select">
|
||||||
|
<AntDesign.Select TItem="PageSizeItem"
|
||||||
|
TItemValue="int"
|
||||||
|
DataSource="PageSizes3"
|
||||||
|
LabelName="Text"
|
||||||
|
ValueName="Value"
|
||||||
|
Value="PageSize"
|
||||||
|
ValueChanged="OnPageSizeChanged">
|
||||||
|
</AntDesign.Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* <BitDropdown Items="PageSizes2"
|
||||||
|
Style="min-width: 7rem"
|
||||||
|
Placeholder="Select..."
|
||||||
|
TItem="BitDropdownItem<int>"
|
||||||
|
TValue="int"
|
||||||
|
Value="PageSize"
|
||||||
|
ValueChanged="OnPageSizeChanged">
|
||||||
|
<PrefixTemplate>
|
||||||
|
<InputPrefix Graphic="Graphic.Grid" Tooltip="Page Size"></InputPrefix>
|
||||||
|
</PrefixTemplate>
|
||||||
|
</BitDropdown> *@
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pagination {
|
||||||
|
padding: 16px 16px;
|
||||||
|
background: transparent;
|
||||||
|
background: #141414;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 12em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public int PageNumber { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> PageNumberChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int[] PageSizes { get; set; } = [5, 10, 25, 50, 100];
|
||||||
|
|
||||||
|
List<BitDropdownItem<int>> PageSizes2 => [.. PageSizes.Select(x => new BitDropdownItem<int>() { Text = x.ToString(), Value = x })];
|
||||||
|
|
||||||
|
List<PageSizeItem> PageSizes3 => [.. PageSizes.Select(x => new PageSizeItem() { Text = $"{x} items / page", Value = x })];
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> PageSizeChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int TotalItems { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<int> TotalItemsChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment? RightContent { get; set; }
|
||||||
|
|
||||||
|
public string IndexInfo => TotalItems == 0 ? "No items" : $"{StartIndex.ToString("n0")} - {EndIndex.ToString("n0")} of {TotalItems.ToString("n0")} items";
|
||||||
|
|
||||||
|
public int StartIndex => (PageNumber - 1) * PageSize + 1;
|
||||||
|
public int EndIndex => PageNumber * PageSize < TotalItems ? PageNumber * PageSize : TotalItems;
|
||||||
|
|
||||||
|
private async Task OnSelectedChanged(int newPage)
|
||||||
|
{
|
||||||
|
PageNumber = newPage;
|
||||||
|
await PageNumberChanged.InvokeAsync(newPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnPageSizeChanged(int newPageSize)
|
||||||
|
{
|
||||||
|
PageSize = newPageSize;
|
||||||
|
await PageSizeChanged.InvokeAsync(newPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnPaginationChanged(AntDesign.PaginationEventArgs args)
|
||||||
|
{
|
||||||
|
int newPage = args.Page;
|
||||||
|
|
||||||
|
PageNumber = newPage;
|
||||||
|
await PageNumberChanged.InvokeAsync(newPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PageSizeItem
|
||||||
|
{
|
||||||
|
public string? Text { get; set; }
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Domain.Enums
|
||||||
@using JSMR.UI.Blazor.Components.Chips
|
@using JSMR.UI.Blazor.Components.Chips
|
||||||
@using JSMR.UI.Blazor.Enums
|
@using JSMR.UI.Blazor.Enums
|
||||||
@@ -7,6 +10,10 @@
|
|||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@using Microsoft.AspNetCore.WebUtilities
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
|
|
||||||
|
@inject VoiceWorksClient Client
|
||||||
|
@inject MessageService MessageService
|
||||||
|
@inject ModalService ModalService
|
||||||
|
|
||||||
<div class=@GetCardClasses(Product)>
|
<div class=@GetCardClasses(Product)>
|
||||||
<div class="j-voice-work-image-container">
|
<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>
|
<JImage OverlayClass="j-voice-work-image-overlay" ImageClass="j-voice-work-image" Source="@ImageUrlProvider.GetImageUrl(Product, "main")" FallbackSource="@ImageUrlProvider.GetImageUrl(Product, "main", "webp")"></JImage>
|
||||||
@@ -15,39 +22,52 @@
|
|||||||
<div class="j-product-title">
|
<div class="j-product-title">
|
||||||
<a href="@Product.ProductUrl" target="_blank">@Product.ProductName</a>
|
<a href="@Product.ProductUrl" target="_blank">@Product.ProductName</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="j-product-contributors">
|
<BitStack Horizontal="true" Gap="0.5em" AutoHeight Wrap>
|
||||||
<span class="j-circle">
|
<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>
|
||||||
<MudChip T="string"
|
|
||||||
Href=@($"https://www.dlsite.com/maniax/circle/profile/=/maker_id/{Product.MakerId}.html")
|
@if (Product.OriginalCircle is not null)
|
||||||
Target="_blank"
|
{
|
||||||
Variant="MudBlazor.Variant.Filled"
|
<Chip Graphic="Graphic.Circle" Varient="ElementVarient.Outlined" Color="ColorVarient.Secondary" Url=@($"https://www.dlsite.com/maniax/circle/profile/=/maker_id/{Product.OriginalCircle.MakerId}.html")>@Product.OriginalCircle.Name</Chip>
|
||||||
Icon="@Icons.Material.Outlined.Circle">@Product.Maker</MudChip>
|
}
|
||||||
@* <CircleChip Circle="@Product.Circle"></CircleChip> *@
|
|
||||||
@foreach (var creator in Product.Creators)
|
@foreach (var creator in Product.Creators)
|
||||||
{
|
{
|
||||||
<MudChip T="string"
|
<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>
|
||||||
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> *@
|
|
||||||
}
|
}
|
||||||
</span>
|
</BitStack>
|
||||||
</div>
|
<BitStack Horizontal="true" Gap="0.5em" AutoHeight Wrap>
|
||||||
|
@foreach (string supportedLanguage in Product.SupportedLanguages)
|
||||||
|
{
|
||||||
|
<Chip Graphic="Graphic.Globe" Varient="ElementVarient.Outlined" Color="ColorVarient.Secondary">@GetSupportedLanguageDescription(supportedLanguage)</Chip>
|
||||||
|
}
|
||||||
|
@if (Product.OriginalProductId is not null)
|
||||||
|
{
|
||||||
|
<Chip Graphic="Graphic.Translate" Varient="ElementVarient.Outlined" Color="ColorVarient.Secondary">Translation</Chip>
|
||||||
|
}
|
||||||
|
@if (Product.Rating == AgeRating.AllAges)
|
||||||
|
{
|
||||||
|
<Chip Graphic="Graphic.Age" Color="ColorVarient.Green" Varient="ElementVarient.Outlined">All Ages</Chip>
|
||||||
|
}
|
||||||
|
else if (Product.Rating == AgeRating.R15)
|
||||||
|
{
|
||||||
|
<Chip Graphic="Graphic.Age" Color="ColorVarient.Blue" Varient="ElementVarient.Outlined">R-15</Chip>
|
||||||
|
}
|
||||||
|
@if (Product.HasTrial || Product.HasChobit)
|
||||||
|
{
|
||||||
|
<Chip Graphic="Graphic.Download" Color="ColorVarient.Yellow" Varient="ElementVarient.Outlined">Trial</Chip>
|
||||||
|
}
|
||||||
|
@if (Product.Favorite)
|
||||||
|
{
|
||||||
|
<Chip Graphic="Graphic.Star" Color="ColorVarient.Teal" Varient="ElementVarient.Outlined">Favorite</Chip>
|
||||||
|
}
|
||||||
|
</BitStack>
|
||||||
<div class="j-product-description">@Product.Description</div>
|
<div class="j-product-description">@Product.Description</div>
|
||||||
<div class="j-tags">
|
<div class="j-tags">
|
||||||
@foreach (var tag in Product.Tags)
|
@foreach (var tag in Product.Tags)
|
||||||
{
|
{
|
||||||
@* <div class="j-tag">@tag.Name</div> *@
|
|
||||||
<ProductTag Tag="tag"></ProductTag>
|
<ProductTag Tag="tag"></ProductTag>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@* <div class="j-tags">
|
|
||||||
@foreach (var tag in Product.Tags)
|
|
||||||
{
|
|
||||||
<TagChip Tag="tag"></TagChip>
|
|
||||||
}
|
|
||||||
</div> *@
|
|
||||||
</div>
|
</div>
|
||||||
<div class="j-voice-work-info">
|
<div class="j-voice-work-info">
|
||||||
<div class="j-release-date-container">
|
<div class="j-release-date-container">
|
||||||
@@ -70,20 +90,45 @@
|
|||||||
<BitStack Horizontal="true" Gap="0.5rem" VerticalAlign="BitAlignment.End" HorizontalAlign="BitAlignment.End">
|
<BitStack Horizontal="true" Gap="0.5rem" VerticalAlign="BitAlignment.End" HorizontalAlign="BitAlignment.End">
|
||||||
@if (Product.IsValid != true)
|
@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)
|
@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)
|
@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)
|
@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>
|
</BitStack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +137,9 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public required VoiceWorkSearchResult Product { get; set; }
|
public required VoiceWorkSearchResult Product { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback ProductDeleted { get; set; }
|
||||||
|
|
||||||
private string GetCardClasses(VoiceWorkSearchResult voiceWork)
|
private string GetCardClasses(VoiceWorkSearchResult voiceWork)
|
||||||
{
|
{
|
||||||
List<string> classNames = ["j-card", "j-voice-work-card"];
|
List<string> classNames = ["j-card", "j-voice-work-card"];
|
||||||
@@ -149,4 +197,87 @@
|
|||||||
return "jp";
|
return "jp";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetSupportedLanguageDescription(string code)
|
||||||
|
{
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case "JPN": return "Japanese";
|
||||||
|
case "ENG": return "English";
|
||||||
|
case "CHI_HANS": return "Chinese (Simplified)";
|
||||||
|
case "CHI_HANT": return "Chinese (Traditional)";
|
||||||
|
case "KO_KR": return "Korean";
|
||||||
|
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">
|
<div class="j-product-items-container">
|
||||||
@foreach (var product in Products)
|
@foreach (var product in Products)
|
||||||
{
|
{
|
||||||
<JProduct Product="@product"></JProduct>
|
<JProduct Product="@product" ProductDeleted="OnProductDeleted"></JProduct>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -21,4 +21,12 @@ else
|
|||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public VoiceWorkSearchResult[]? Products { get; set; }
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
@using JSMR.Application.Tags.Queries.Search.Contracts
|
@using JSMR.Application.Tags.Queries.Search.Contracts
|
||||||
@using JSMR.Application.VoiceWorks.Queries.Search
|
@using JSMR.Application.VoiceWorks.Queries.Search
|
||||||
|
@using JSMR.UI.Blazor.Enums
|
||||||
@using JSMR.UI.Blazor.Filters
|
@using JSMR.UI.Blazor.Filters
|
||||||
@using JSMR.UI.Blazor.Services
|
@using JSMR.UI.Blazor.Services
|
||||||
@using Microsoft.AspNetCore.WebUtilities
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
|
|
||||||
<a class="@Classes" @onclick="@OnClick"><Icon Graphic="Enums.Graphic.Tag" Color="Enums.ColorVarient.Primary"></Icon>@Tag.Name</a>
|
@* <a class="@Classes" @onclick="@OnClick"><Icon Graphic="Enums.Graphic.Tag" Color="Enums.ColorVarient.Primary"></Icon>@Tag.Name</a> *@
|
||||||
@* <MudChip T="string" Icon="@Icons.Material.Outlined.Tag" @onclick="@OnClick" Variant="@MudBlazor.Variant.Filled" Color="@MudBlazor.Color.Surface">@Tag.Name</MudChip> *@
|
@* <MudChip T="string" Icon="@Icons.Material.Outlined.Tag" @onclick="@OnClick" Variant="@MudBlazor.Variant.Filled" Color="@MudBlazor.Color.Surface">@Tag.Name</MudChip> *@
|
||||||
|
<Chip Graphic="Graphic.Tag" Varient="ElementVarient.Outlined" Color="@GetColorVarient()" Tone="ToneVarient.None" Click="@OnClick">@Tag.Name</Chip>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Inject]
|
[Inject]
|
||||||
@@ -33,6 +35,21 @@
|
|||||||
return string.Join(" ", classNames);
|
return string.Join(" ", classNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ColorVarient GetColorVarient()
|
||||||
|
{
|
||||||
|
// if (Tag.IsFavorite)
|
||||||
|
// {
|
||||||
|
// return ColorVarient.Mint;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (Tag.IsBlacklisted)
|
||||||
|
// {
|
||||||
|
// return ColorVarient.Red;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return ColorVarient.Primary;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnClick()
|
private void OnClick()
|
||||||
{
|
{
|
||||||
VoiceWorkFilterState state = new()
|
VoiceWorkFilterState state = new()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@using JSMR.UI.Blazor.Enums
|
@using JSMR.UI.Blazor.Enums
|
||||||
@using JSMR.UI.Blazor.Filters
|
@using JSMR.UI.Blazor.Filters
|
||||||
@using JSMR.UI.Blazor.Services
|
@using JSMR.UI.Blazor.Services
|
||||||
|
@using AntDesign
|
||||||
|
|
||||||
<div class="search-toolbar">
|
<div class="search-toolbar">
|
||||||
<BitMenuButton TItem="BitMenuButtonItem" Text="Presets" Items="presets" Variant="BitVariant.Outline" OnClick="test" />
|
<BitMenuButton TItem="BitMenuButtonItem" Text="Presets" Items="presets" Variant="BitVariant.Outline" OnClick="test" />
|
||||||
@@ -59,9 +60,18 @@
|
|||||||
Value="@Value.SaleStatus"
|
Value="@Value.SaleStatus"
|
||||||
ValueChanged="@(value => Update(Value with { SaleStatus = value }))">
|
ValueChanged="@(value => Update(Value with { SaleStatus = value }))">
|
||||||
<PrefixTemplate>
|
<PrefixTemplate>
|
||||||
<InputPrefix Graphic="Graphic.Cart" Tooltip="Keywords"></InputPrefix>
|
<InputPrefix Graphic="Graphic.Cart" Tooltip="Sale Status"></InputPrefix>
|
||||||
</PrefixTemplate>
|
</PrefixTemplate>
|
||||||
</BitDropdown>
|
</BitDropdown>
|
||||||
|
@* <Select TItem="SelectOption<SaleStatus?>"
|
||||||
|
TItemValue="SaleStatus?"
|
||||||
|
Placeholder="All (Placeholder)"
|
||||||
|
ValueName="Value"
|
||||||
|
LabelName="Label"
|
||||||
|
DataSource="saleStatuses2"
|
||||||
|
Value="@Value.SaleStatus"
|
||||||
|
ValueChanged="@(value => Update(Value with { SaleStatus = value }))">
|
||||||
|
</Select> *@
|
||||||
</div>
|
</div>
|
||||||
<div class="search-filter-control-span-1">
|
<div class="search-filter-control-span-1">
|
||||||
<BitDropdown Prefix="Circles"
|
<BitDropdown Prefix="Circles"
|
||||||
@@ -219,6 +229,7 @@
|
|||||||
|
|
||||||
List<BitDropdownItem<Locale>> locales = [];
|
List<BitDropdownItem<Locale>> locales = [];
|
||||||
List<BitDropdownItem<SaleStatus?>> saleStatuses = [];
|
List<BitDropdownItem<SaleStatus?>> saleStatuses = [];
|
||||||
|
List<SelectOption<SaleStatus?>> saleStatuses2 = [];
|
||||||
List<BitDropdownItem<CircleStatus?>> circleStatuses = [];
|
List<BitDropdownItem<CircleStatus?>> circleStatuses = [];
|
||||||
List<BitDropdownItem<TagStatus?>> tagStatuses = [];
|
List<BitDropdownItem<TagStatus?>> tagStatuses = [];
|
||||||
List<BitDropdownItem<CreatorStatus?>> creatorStatuses = [];
|
List<BitDropdownItem<CreatorStatus?>> creatorStatuses = [];
|
||||||
@@ -242,6 +253,7 @@
|
|||||||
{
|
{
|
||||||
locales = Lookups.GetLocales();
|
locales = Lookups.GetLocales();
|
||||||
saleStatuses = Lookups.GetSaleStatuses();
|
saleStatuses = Lookups.GetSaleStatuses();
|
||||||
|
saleStatuses2 = Lookups.GetSaleStatuses2();
|
||||||
circleStatuses = Lookups.GetCircleStatuses();
|
circleStatuses = Lookups.GetCircleStatuses();
|
||||||
tagStatuses = Lookups.GetTagStatuses();
|
tagStatuses = Lookups.GetTagStatuses();
|
||||||
creatorStatuses = Lookups.GetCreatorStatuses();
|
creatorStatuses = Lookups.GetCreatorStatuses();
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ public enum ColorVarient
|
|||||||
Teal,
|
Teal,
|
||||||
Blue,
|
Blue,
|
||||||
Orange,
|
Orange,
|
||||||
Pink
|
Pink,
|
||||||
|
Red
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CssUtil
|
public static class CssUtil
|
||||||
|
|||||||
@@ -19,5 +19,8 @@ public enum Graphic
|
|||||||
Sort,
|
Sort,
|
||||||
Grid,
|
Grid,
|
||||||
Age,
|
Age,
|
||||||
Calendar
|
Calendar,
|
||||||
|
Download,
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -10,14 +10,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bit.BlazorUI" Version="10.4.2" />
|
<PackageReference Include="AntDesign" Version="1.6.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.4" />
|
<PackageReference Include="Bit.BlazorUI" Version="10.4.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.4" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.8" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.10" />
|
||||||
<PackageReference Include="MudBlazor" Version="9.1.0" />
|
<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="Newtonsoft.Json" Version="13.0.4" />
|
||||||
<PackageReference Include="Radzen.Blazor" Version="9.1.0" />
|
<PackageReference Include="Radzen.Blazor" Version="10.4.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@using JSMR.UI.Blazor.Components
|
@using AntDesign
|
||||||
|
@using JSMR.UI.Blazor.Components
|
||||||
@using JSMR.UI.Blazor.Services
|
@using JSMR.UI.Blazor.Services
|
||||||
|
|
||||||
@inject SessionState Session
|
@inject SessionState Session
|
||||||
@@ -6,9 +7,11 @@
|
|||||||
|
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<AntContainer />
|
||||||
|
|
||||||
<MudLayout>
|
<MudLayout>
|
||||||
<MudAppBar Elevation="1" Dense="@_dense">
|
<MudAppBar Elevation="1" Dense="@_dense">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
|
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="MudBlazor.Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
|
||||||
<MudText>JSMR</MudText>
|
<MudText>JSMR</MudText>
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
@* <MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/MudBlazor/MudBlazor" Target="_blank" /> *@
|
@* <MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/MudBlazor/MudBlazor" Target="_blank" /> *@
|
||||||
@@ -40,6 +43,7 @@
|
|||||||
<MudMainContent Class="pt-18 px-8">
|
<MudMainContent Class="pt-18 px-8">
|
||||||
@Body
|
@Body
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
|
|
||||||
@* Required *@
|
@* Required *@
|
||||||
<MudThemeProvider @ref="_mudThemeProvider" @bind-IsDarkMode="_isDarkMode" />
|
<MudThemeProvider @ref="_mudThemeProvider" @bind-IsDarkMode="_isDarkMode" />
|
||||||
<MudPopoverProvider />
|
<MudPopoverProvider />
|
||||||
@@ -49,6 +53,7 @@
|
|||||||
|
|
||||||
@* Needed for snackbars *@
|
@* Needed for snackbars *@
|
||||||
<MudSnackbarProvider />
|
<MudSnackbarProvider />
|
||||||
|
|
||||||
</MudLayout>
|
</MudLayout>
|
||||||
|
|
||||||
<RadzenComponents @rendermode="RenderMode.InteractiveAuto" />
|
<RadzenComponents @rendermode="RenderMode.InteractiveAuto" />
|
||||||
|
|||||||
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; }
|
||||||
|
}
|
||||||
@@ -39,15 +39,15 @@ else
|
|||||||
|
|
||||||
@if (item.Favorite)
|
@if (item.Favorite)
|
||||||
{
|
{
|
||||||
<MudChip T="string" Label="true" Color="Color.Info" Style="width: 100%" Variant="MudBlazor.Variant.Outlined">Favorite</MudChip>
|
<MudChip T="string" Label="true" Color="MudBlazor.Color.Info" Style="width: 100%" Variant="MudBlazor.Variant.Outlined">Favorite</MudChip>
|
||||||
}
|
}
|
||||||
else if (item.Blacklisted)
|
else if (item.Blacklisted)
|
||||||
{
|
{
|
||||||
<MudChip T="string" Label="true" Color="Color.Warning" Style="width: 100%" Variant="MudBlazor.Variant.Outlined">Blacklisted</MudChip>
|
<MudChip T="string" Label="true" Color="MudBlazor.Color.Warning" Style="width: 100%" Variant="MudBlazor.Variant.Outlined">Blacklisted</MudChip>
|
||||||
}
|
}
|
||||||
else if (item.Spam)
|
else if (item.Spam)
|
||||||
{
|
{
|
||||||
<MudChip T="string" Label="true" Color="Color.Error" Style="width: 100%" Variant="MudBlazor.Variant.Outlined">Spam</MudChip>
|
<MudChip T="string" Label="true" Color="MudBlazor.Color.Error" Style="width: 100%" Variant="MudBlazor.Variant.Outlined">Spam</MudChip>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,55 +1,120 @@
|
|||||||
@page "/creators"
|
@page "/creators"
|
||||||
@inject VoiceWorksClient Client
|
@inject VoiceWorksClient Client
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@using AntDesign
|
||||||
@using JSMR.Application.Common.Search
|
@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
|
||||||
@using JSMR.Application.Creators.Queries.Search.Contracts
|
@using JSMR.Application.Creators.Queries.Search.Contracts
|
||||||
@using JSMR.UI.Blazor.Components
|
@using JSMR.UI.Blazor.Components
|
||||||
|
@using JSMR.UI.Blazor.Filters
|
||||||
@using JSMR.UI.Blazor.Services
|
@using JSMR.UI.Blazor.Services
|
||||||
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
|
|
||||||
<PageTitle>Creators</PageTitle>
|
<PageTitle>Creators</PageTitle>
|
||||||
|
|
||||||
<h1>Creators</h1>
|
@* <h1>Creators</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" />
|
<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" />
|
||||||
|
*@
|
||||||
|
|
||||||
@if (searchResults is null)
|
<AntDesign.Card Title=@("Creators") Class="ant-blurred-card">
|
||||||
|
<Extra>
|
||||||
|
<AntDesign.Input TValue="string" Value="Keywords" ValueChanged="OnKeywordsChanged" DebounceMilliseconds="500" Placeholder="Filter">
|
||||||
|
<Prefix>
|
||||||
|
<AntDesign.Icon Type="@AntDesign.IconType.Outline.Search" />
|
||||||
|
</Prefix>
|
||||||
|
</AntDesign.Input>
|
||||||
|
</Extra>
|
||||||
|
<Body>
|
||||||
|
<AntDesign.Table DataSource="@(searchResults?.Items ?? Enumerable.Empty<CreatorSearchItem>())"
|
||||||
|
Total="@(searchResults?.TotalItems ?? 0)"
|
||||||
|
TItem="CreatorSearchItem"
|
||||||
|
Loading="LoadingData"
|
||||||
|
HidePagination="@true"
|
||||||
|
RemoteDataSource="@true"
|
||||||
|
RowKey="x=>x.CreatorId"
|
||||||
|
OnChange="HandleTableChange">
|
||||||
|
<ColumnDefinitions>
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.Name" Title="Name" Sortable="true" SorterMultiple="4" />
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.VoiceWorkCount" Title="Voice Works" Format="n0" HeaderStyle="width: 10em" Sortable="true" SorterMultiple="4">
|
||||||
|
<AntDesign.Button Size="AntDesign.ButtonSize.Small" Type="AntDesign.ButtonType.Link" OnClick="@(e => NavigateToVoiceWorkSearch(context))" Style="display:flex;justify-self:flex-end;">@context.VoiceWorkCount.ToString("n0")</AntDesign.Button>
|
||||||
|
</AntDesign.PropertyColumn>
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.Favorite" Title="Favorite" HeaderStyle="width: 8em" Sortable="true" SortDirections="new[] { AntDesign.SortDirection.Descending }" SorterMultiple="4">
|
||||||
|
@if (context.Favorite)
|
||||||
{
|
{
|
||||||
<p>Loading…</p>
|
<AntDesign.Tag Color="AntDesign.TagColor.PurpleInverse">Favorite</AntDesign.Tag>
|
||||||
}
|
}
|
||||||
else if (searchResults.Items.Length == 0)
|
</AntDesign.PropertyColumn>
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.Blacklisted" Title="Blacklisted" HeaderStyle="width: 9em" Sortable="true" SortDirections="new[] { AntDesign.SortDirection.Descending }" SorterMultiple="4">
|
||||||
|
@if (context.Blacklisted)
|
||||||
{
|
{
|
||||||
<p>No results.</p>
|
<AntDesign.Tag Color="AntDesign.TagColor.RedInverse">Blacklisted</AntDesign.Tag>
|
||||||
}
|
}
|
||||||
else
|
</AntDesign.PropertyColumn>
|
||||||
|
<AntDesign.ActionColumn HeaderStyle="width: 5em;" Style="text-align: center">
|
||||||
|
<Dropdown Trigger="@([Trigger.Click])">
|
||||||
|
<Overlay>
|
||||||
|
<Menu Selectable="false">
|
||||||
|
@if (!context.Favorite)
|
||||||
{
|
{
|
||||||
<MudTable Items="@searchResults.Items" Style="table-layout: fixed" Virtualize="true">
|
<MenuItem OnClick="(e) => SetStatus(context, CreatorStatus.Favorite)">Set as Favorite</MenuItem>
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>Name</MudTh>
|
|
||||||
<MudTh>Voice Works</MudTh>
|
|
||||||
<MudTh>Favorite</MudTh>
|
|
||||||
<MudTh>Blacklisted</MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd DataLabel="Name">@context.Name</MudTd>
|
|
||||||
<MudTd DataLabel="Voice Works">@context.VoiceWorkCount</MudTd>
|
|
||||||
<MudTd DataLabel="Favorite">
|
|
||||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(context.Favorite? Color.Secondary: Color.Dark)" />
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Blacklisted">
|
|
||||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(context.Blacklisted? Color.Secondary: Color.Dark)" />
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
</MudTable>
|
|
||||||
|
|
||||||
<JPagination PageNumber="PageNumber" PageNumberChanged="OnPageNumberChanged" PageSize="PageSize" PageSizeChanged="OnPageSizeChanged" @bind-TotalItems="searchResults.TotalItems" />
|
|
||||||
}
|
}
|
||||||
|
@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>
|
||||||
|
<Button Icon="Ellipsis"></Button>
|
||||||
|
</ChildContent>
|
||||||
|
</Dropdown>
|
||||||
|
</AntDesign.ActionColumn>
|
||||||
|
</ColumnDefinitions>
|
||||||
|
</AntDesign.Table>
|
||||||
|
<JPagination2 PageNumber="PageNumber" PageNumberChanged="OnPageNumberChanged" PageSize="PageSize" PageSizeChanged="OnPageSizeChanged" TotalItems="TotalItems" />
|
||||||
|
</Body>
|
||||||
|
</AntDesign.Card>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.mud-table-root {
|
.mud-table-root {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url(https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ETkNSJ-oUGwAAAAAQ_AAAAgAegCCAQ/original);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-blurred-card {
|
||||||
|
background-color: color-mix(in srgb, #141414 70%, transparent);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-extra {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table {
|
||||||
|
/* background: inherit; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
.j-pager {
|
.j-pager {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -63,15 +128,24 @@ else
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[Inject]
|
||||||
|
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
INotificationService NotificationService { get; set; } = default!;
|
||||||
|
|
||||||
public string? Keywords { get; set; }
|
public string? Keywords { get; set; }
|
||||||
public int PageNumber { get; set; } = 1;
|
public int PageNumber { get; set; } = 1;
|
||||||
public int PageSize { get; set; } = 100;
|
public int PageSize { get; set; } = 100;
|
||||||
|
public int TotalItems => searchResults?.TotalItems ?? 0;
|
||||||
|
|
||||||
|
public bool LoadingData { get; set; }
|
||||||
|
|
||||||
SearchResult<CreatorSearchItem>? searchResults;
|
SearchResult<CreatorSearchItem>? searchResults;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await UpdateDataAsync(true);
|
//await UpdateDataAsync(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnKeywordsChanged(string? newKeywords)
|
public async Task OnKeywordsChanged(string? newKeywords)
|
||||||
@@ -102,6 +176,8 @@ else
|
|||||||
|
|
||||||
private async Task LoadCreatorsAsync()
|
private async Task LoadCreatorsAsync()
|
||||||
{
|
{
|
||||||
|
LoadingData = true;
|
||||||
|
|
||||||
SearchCreatorsRequest request = new(
|
SearchCreatorsRequest request = new(
|
||||||
Options: new()
|
Options: new()
|
||||||
{
|
{
|
||||||
@@ -111,10 +187,7 @@ else
|
|||||||
{
|
{
|
||||||
Name = Keywords
|
Name = Keywords
|
||||||
},
|
},
|
||||||
SortOptions =
|
SortOptions = [.. _sortOptions]
|
||||||
[
|
|
||||||
new(CreatorSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -123,6 +196,109 @@ else
|
|||||||
|
|
||||||
searchResults = result?.Results ?? new();
|
searchResults = result?.Results ?? new();
|
||||||
|
|
||||||
|
LoadingData = false;
|
||||||
|
|
||||||
//await InvokeAsync(StateHasChanged);
|
//await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SortOption<CreatorSortField>> _sortOptions =
|
||||||
|
[
|
||||||
|
new(CreatorSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
||||||
|
];
|
||||||
|
|
||||||
|
private async Task HandleTableChange(AntDesign.TableModels.QueryModel<CreatorSearchItem> queryModel)
|
||||||
|
{
|
||||||
|
//PageNumber = queryModel.PageIndex;
|
||||||
|
//PageSize = queryModel.PageSize;
|
||||||
|
|
||||||
|
_sortOptions = MapSortOptions(queryModel);
|
||||||
|
|
||||||
|
await LoadCreatorsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SortOption<CreatorSortField>> MapSortOptions(AntDesign.TableModels.QueryModel<CreatorSearchItem> queryModel)
|
||||||
|
{
|
||||||
|
var requestedSorts = queryModel.SortModel
|
||||||
|
.Select(sort => new
|
||||||
|
{
|
||||||
|
Field = sort.FieldName switch
|
||||||
|
{
|
||||||
|
nameof(CreatorSearchItem.Favorite) => CreatorSortField.Favorite,
|
||||||
|
nameof(CreatorSearchItem.Blacklisted) => CreatorSortField.Blacklisted,
|
||||||
|
nameof(CreatorSearchItem.VoiceWorkCount) => CreatorSortField.VoiceWorkCount,
|
||||||
|
nameof(CreatorSearchItem.Name) => CreatorSortField.Name,
|
||||||
|
_ => (CreatorSortField?)null
|
||||||
|
},
|
||||||
|
Direction = sort.SortDirection switch
|
||||||
|
{
|
||||||
|
AntDesign.SortDirection.Ascending => Application.Common.Search.SortDirection.Ascending,
|
||||||
|
AntDesign.SortDirection.Descending => Application.Common.Search.SortDirection.Descending,
|
||||||
|
_ => (Application.Common.Search.SortDirection?)null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Where(x => x.Field is not null && x.Direction is not null)
|
||||||
|
.ToDictionary(x => x.Field!.Value, x => x.Direction!.Value);
|
||||||
|
|
||||||
|
var finalSorts = new List<SortOption<CreatorSortField>>();
|
||||||
|
|
||||||
|
// Force your preferred precedence
|
||||||
|
if (requestedSorts.TryGetValue(CreatorSortField.Favorite, out var favoriteDir))
|
||||||
|
finalSorts.Add(new(CreatorSortField.Favorite, favoriteDir));
|
||||||
|
|
||||||
|
if (requestedSorts.TryGetValue(CreatorSortField.Blacklisted, out var blacklistedDir))
|
||||||
|
finalSorts.Add(new(CreatorSortField.Blacklisted, blacklistedDir));
|
||||||
|
|
||||||
|
if (requestedSorts.TryGetValue(CreatorSortField.VoiceWorkCount, out var countDir))
|
||||||
|
finalSorts.Add(new(CreatorSortField.VoiceWorkCount, countDir));
|
||||||
|
|
||||||
|
if (requestedSorts.TryGetValue(CreatorSortField.Name, out var nameDir))
|
||||||
|
finalSorts.Add(new(CreatorSortField.Name, nameDir));
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
if (finalSorts.Count == 0)
|
||||||
|
finalSorts.Add(new(CreatorSortField.Name, Application.Common.Search.SortDirection.Ascending));
|
||||||
|
|
||||||
|
return finalSorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigateToVoiceWorkSearch(CreatorSearchItem item)
|
||||||
|
{
|
||||||
|
VoiceWorkFilterState state = new()
|
||||||
|
{
|
||||||
|
CreatorIds = [item.CreatorId]
|
||||||
|
};
|
||||||
|
|
||||||
|
//string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Path);
|
||||||
|
string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Authority);
|
||||||
|
string uri = QueryHelpers.AddQueryString($"{basePath}/voiceworks", state.ToQuery());
|
||||||
|
|
||||||
|
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">
|
<BitPivot Size="BitSize.Medium">
|
||||||
<BitPivotItem HeaderText="@($"Available ({availableVoiceWorks?.Length ?? 0})")">
|
<BitPivotItem HeaderText="@($"Available ({availableVoiceWorks?.Length ?? 0})")">
|
||||||
<JProductCollection Products="availableVoiceWorks"></JProductCollection>
|
<JProductCollection Products="availableVoiceWorks" ProductDeleted="OnAvailableProductDeleted"></JProductCollection>
|
||||||
</BitPivotItem>
|
</BitPivotItem>
|
||||||
<BitPivotItem HeaderText="@($"Upcoming ({upcomingVoiceWorks?.Length ?? 0})")">
|
<BitPivotItem HeaderText="@($"Upcoming ({upcomingVoiceWorks?.Length ?? 0})")">
|
||||||
<JProductCollection Products="upcomingVoiceWorks"></JProductCollection>
|
<JProductCollection Products="upcomingVoiceWorks" ProductDeleted="OnUpcomingProductDeleted"></JProductCollection>
|
||||||
</BitPivotItem>
|
</BitPivotItem>
|
||||||
<BitPivotItem HeaderText="@($"Announcements ({announcedVoiceWorks?.Length ?? 0})")">
|
<BitPivotItem HeaderText="@($"Announcements ({announcedVoiceWorks?.Length ?? 0})")">
|
||||||
<JProductCollection Products="announcedVoiceWorks"></JProductCollection>
|
<JProductCollection Products="announcedVoiceWorks" ProductDeleted="OnAnnouncedProductDeleted"></JProductCollection>
|
||||||
</BitPivotItem>
|
</BitPivotItem>
|
||||||
</BitPivot>
|
</BitPivot>
|
||||||
|
|
||||||
@@ -105,4 +105,21 @@
|
|||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
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"
|
@page "/login"
|
||||||
@layout LoginLayout
|
@layout LoginLayout
|
||||||
|
|
||||||
|
@using AntDesign
|
||||||
@using JSMR.UI.Blazor.Services
|
@using JSMR.UI.Blazor.Services
|
||||||
|
@using MudBlazor.Charts
|
||||||
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
|
||||||
@inject SessionState Session
|
@inject SessionState Session
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
<h3>Login</h3>
|
@* <h3>Login</h3> *@
|
||||||
|
|
||||||
|
<PageTitle>Sign In - JSMR</PageTitle>
|
||||||
|
|
||||||
@if (Session.IsAuthenticated)
|
@if (Session.IsAuthenticated)
|
||||||
{
|
{
|
||||||
@@ -15,27 +20,83 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div style="max-width: 360px;">
|
<div class="login-container">
|
||||||
<BitCard>
|
<AntDesign.Card Title=@("Sign In") Class="ant-blurred-card">
|
||||||
<BitStack>
|
<Body>
|
||||||
<BitTextField Label="Username" @bind-Value="username"></BitTextField>
|
<AntDesign.Form Model="@loginModel" Layout="FormLayout.Vertical">
|
||||||
<BitTextField Label="Password" @bind-Value="password" Type="BitInputType.Password"></BitTextField>
|
<AntDesign.FormItem Label="Username">
|
||||||
<BitButton OnClick="LoginAsync" IsEnabled="@(!busy)">Login</BitButton>
|
<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))
|
@if (!string.IsNullOrWhiteSpace(error))
|
||||||
{
|
{
|
||||||
<p style="color: crimson; margin-top: 8px;">@error</p>
|
<Alert Type="AlertType.Error" Message="@error" ShowIcon="false" />
|
||||||
}
|
}
|
||||||
</BitStack>
|
|
||||||
</BitCard>
|
|
||||||
</div>
|
</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 {
|
@code {
|
||||||
private string username = "";
|
private string username = "";
|
||||||
private string password = "";
|
private string password = "";
|
||||||
private bool busy;
|
private bool busy;
|
||||||
private string? error;
|
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()
|
private async Task LoginAsync()
|
||||||
{
|
{
|
||||||
busy = true;
|
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()
|
private async Task Logout()
|
||||||
{
|
{
|
||||||
busy = true;
|
busy = true;
|
||||||
|
|||||||
@@ -1,54 +1,145 @@
|
|||||||
@page "/tags"
|
@page "/tags"
|
||||||
@inject VoiceWorksClient Client
|
@inject VoiceWorksClient Client
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@using AntDesign
|
||||||
@using JSMR.Application.Common.Search
|
@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
|
||||||
@using JSMR.Application.Tags.Queries.Search.Contracts
|
@using JSMR.Application.Tags.Queries.Search.Contracts
|
||||||
@using JSMR.UI.Blazor.Components
|
@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.Services
|
||||||
|
@using JSMR.UI.Blazor.Shared
|
||||||
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
|
@using System.Text.Json
|
||||||
|
|
||||||
|
@inherits SearchPageBase<TagFilterState, TagSearchItem>
|
||||||
|
|
||||||
<PageTitle>Tags</PageTitle>
|
<PageTitle>Tags</PageTitle>
|
||||||
|
|
||||||
<h1>Tags</h1>
|
<div class="fdsfds">
|
||||||
|
<AntDesign.Card Title=@("Tags") Class="ant-blurred-card">
|
||||||
<MudTextField @bind-Value="Keywords" Immediate="true" DebounceInterval="500" Label="Filter" Variant="MudBlazor.Variant.Text" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" />
|
<Extra>
|
||||||
|
<AntDesign.Input TValue="string" Value="State.Keywords" ValueChanged="OnKeywordsChanged" DebounceMilliseconds="500" Placeholder="Filter">
|
||||||
@if (searchResults is null)
|
<Prefix>
|
||||||
|
<AntDesign.Icon Type="@AntDesign.IconType.Outline.Search" />
|
||||||
|
</Prefix>
|
||||||
|
</AntDesign.Input>
|
||||||
|
</Extra>
|
||||||
|
<Body>
|
||||||
|
<AntDesign.Table Responsive
|
||||||
|
DataSource="@(Result?.Items ?? Enumerable.Empty<TagSearchItem>())"
|
||||||
|
Total="@(Result?.TotalItems ?? 0)"
|
||||||
|
TItem="TagSearchItem"
|
||||||
|
Loading="IsLoading"
|
||||||
|
HidePagination="@true"
|
||||||
|
RemoteDataSource="@true"
|
||||||
|
RowKey="x=>x.TagId"
|
||||||
|
OnChange="HandleTableChange">
|
||||||
|
<ColumnDefinitions>
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.Name" Title="Name" Sortable SorterMultiple="5" />
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.EnglishName" Title="English Name" Sortable SorterMultiple="5" />
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.VoiceWorkCount" Title="Voice Works" Format="n0" HeaderStyle="width: 10em" Sortable SorterMultiple="5">
|
||||||
|
<AntDesign.Button Size="AntDesign.ButtonSize.Small" Type="AntDesign.ButtonType.Link" OnClick="@(e => NavigateToVoiceWorkSearch(context))" Style="display:flex;justify-self:flex-end;">@context.VoiceWorkCount.ToString("n0")</AntDesign.Button>
|
||||||
|
</AntDesign.PropertyColumn>
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.Favorite" Title="Favorite" HeaderStyle="width: 8em" Sortable SortDirections="new[] { AntDesign.SortDirection.Descending }" SorterMultiple="5">
|
||||||
|
@if (context.Favorite)
|
||||||
{
|
{
|
||||||
<p>Loading…</p>
|
<AntDesign.Tag Color="AntDesign.TagColor.PurpleInverse">Favorite</AntDesign.Tag>
|
||||||
}
|
}
|
||||||
else if (searchResults.Items.Length == 0)
|
</AntDesign.PropertyColumn>
|
||||||
|
<AntDesign.PropertyColumn Property="c => c.Blacklisted" Title="Blacklisted" HeaderStyle="width: 9em" Sortable SortDirections="new[] { AntDesign.SortDirection.Descending }" SorterMultiple="5">
|
||||||
|
@if (context.Blacklisted)
|
||||||
{
|
{
|
||||||
<p>No results.</p>
|
<AntDesign.Tag Color="AntDesign.TagColor.RedInverse">Blacklisted</AntDesign.Tag>
|
||||||
}
|
}
|
||||||
else
|
</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)
|
||||||
{
|
{
|
||||||
<MudDataGrid Items="@searchResults.Items" Style="table-layout: fixed">
|
<MenuItem OnClick="(e) => SetStatus(context, TagStatus.Favorite)">Favorite</MenuItem>
|
||||||
<Columns>
|
|
||||||
<PropertyColumn Property="x => x.Name" Title="Name" />
|
|
||||||
<PropertyColumn Property="x => x.EnglishName" Title="English Name" />
|
|
||||||
<PropertyColumn Property="x => x.VoiceWorkCount" Title="Voice Works" HeaderStyle="width: 14em" />
|
|
||||||
<TemplateColumn Title="Favorite" HeaderStyle="width: 8em">
|
|
||||||
<CellTemplate>
|
|
||||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(context.Item.Favorite ? Color.Secondary : Color.Dark)" @onclick="@(e => IncrementCount(context.Item))" />
|
|
||||||
</CellTemplate>
|
|
||||||
</TemplateColumn>
|
|
||||||
<TemplateColumn Title="Blacklisted" HeaderStyle="width: 8em">
|
|
||||||
<CellTemplate>
|
|
||||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(context.Item.Blacklisted? Color.Secondary: Color.Dark)" />
|
|
||||||
</CellTemplate>
|
|
||||||
</TemplateColumn>
|
|
||||||
</Columns>
|
|
||||||
</MudDataGrid>
|
|
||||||
|
|
||||||
<JPagination @bind-PageNumber="PageNumber" @bind-PageSize="PageSize" @bind-TotalItems="searchResults.TotalItems" />
|
|
||||||
}
|
}
|
||||||
|
@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="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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.mud-table-root {
|
.mud-table-root {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url(https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ETkNSJ-oUGwAAAAAQ_AAAAgAegCCAQ/original);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-blurred-card {
|
||||||
|
background-color: color-mix(in srgb, #141414 70%, transparent);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-extra {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table {
|
||||||
|
/* background: inherit; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-pagination {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 16px 0;
|
||||||
|
margin: 0;
|
||||||
|
background: #141414;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-pagination.ant-pagination {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.j-pager {
|
.j-pager {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -62,87 +153,226 @@ else
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string? keywords;
|
[Inject]
|
||||||
|
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||||
|
|
||||||
public string? Keywords
|
[Inject]
|
||||||
|
INotificationService NotificationService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
IMessageService MessageService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
ModalService ModalService { get; set; } = default!;
|
||||||
|
|
||||||
|
public async Task OnKeywordsChanged(string? newKeywords)
|
||||||
{
|
{
|
||||||
get { return keywords; }
|
await UpdateAsync(State with
|
||||||
set
|
|
||||||
{
|
{
|
||||||
keywords = value;
|
Keywords = newKeywords,
|
||||||
_ = UpdateDataAsync(true);
|
PageNumber = 1
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private int pageNumber = 1;
|
private async Task HandleTableChange(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
||||||
|
{
|
||||||
|
if (IsLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
public int PageNumber
|
var nextSortOptions = MapSortOptions(queryModel);
|
||||||
|
|
||||||
|
if (SortOptionsEqual(nextSortOptions, State.SortOptions))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await UpdateAsync(State with
|
||||||
{
|
{
|
||||||
get { return pageNumber; }
|
SortOptions = nextSortOptions,
|
||||||
set
|
PageNumber = 1
|
||||||
{
|
});
|
||||||
pageNumber = value;
|
|
||||||
_ = UpdateDataAsync(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int pageSize = 100;
|
private List<SortOption<TagSortField>> MapSortOptions(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
||||||
|
|
||||||
public int PageSize
|
|
||||||
{
|
{
|
||||||
get { return pageSize; }
|
var requestedSorts = queryModel.SortModel
|
||||||
set
|
.Select(sort => new
|
||||||
{
|
{
|
||||||
pageSize = value;
|
Field = sort.FieldName switch
|
||||||
_ = UpdateDataAsync(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchResult<TagSearchItem>? searchResults;
|
|
||||||
|
|
||||||
protected override Task OnInitializedAsync()
|
|
||||||
{
|
{
|
||||||
_ = LoadTagsAsync();
|
nameof(TagSearchItem.Favorite) => TagSortField.Favorite,
|
||||||
|
nameof(TagSearchItem.Blacklisted) => TagSortField.Blacklisted,
|
||||||
return Task.CompletedTask;
|
nameof(TagSearchItem.VoiceWorkCount) => TagSortField.VoiceWorkCount,
|
||||||
}
|
nameof(TagSearchItem.EnglishName) => TagSortField.EnglishName,
|
||||||
|
nameof(TagSearchItem.Name) => TagSortField.Name,
|
||||||
private async Task UpdateDataAsync(bool resetPageNumber)
|
_ => (TagSortField?)null
|
||||||
{
|
|
||||||
if (resetPageNumber)
|
|
||||||
pageNumber = 1;
|
|
||||||
|
|
||||||
await LoadTagsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadTagsAsync()
|
|
||||||
{
|
|
||||||
SearchTagsRequest request = new(
|
|
||||||
Options: new()
|
|
||||||
{
|
|
||||||
PageNumber = PageNumber,
|
|
||||||
PageSize = pageSize,
|
|
||||||
Criteria = new()
|
|
||||||
{
|
|
||||||
Name = Keywords
|
|
||||||
},
|
},
|
||||||
SortOptions =
|
Direction = sort.SortDirection switch
|
||||||
[
|
{
|
||||||
new(TagSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
AntDesign.SortDirection.Ascending => Application.Common.Search.SortDirection.Ascending,
|
||||||
]
|
AntDesign.SortDirection.Descending => Application.Common.Search.SortDirection.Descending,
|
||||||
|
_ => (Application.Common.Search.SortDirection?)null
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.Where(x => x.Field is not null && x.Direction is not null)
|
||||||
|
.ToDictionary(x => x.Field!.Value, x => x.Direction!.Value);
|
||||||
|
|
||||||
|
var finalSorts = new List<SortOption<TagSortField>>();
|
||||||
|
|
||||||
|
// Force your preferred precedence
|
||||||
|
if (requestedSorts.TryGetValue(TagSortField.Favorite, out var favoriteDir))
|
||||||
|
finalSorts.Add(new(TagSortField.Favorite, favoriteDir));
|
||||||
|
|
||||||
|
if (requestedSorts.TryGetValue(TagSortField.Blacklisted, out var blacklistedDir))
|
||||||
|
finalSorts.Add(new(TagSortField.Blacklisted, blacklistedDir));
|
||||||
|
|
||||||
|
if (requestedSorts.TryGetValue(TagSortField.VoiceWorkCount, out var countDir))
|
||||||
|
finalSorts.Add(new(TagSortField.VoiceWorkCount, countDir));
|
||||||
|
|
||||||
|
if (requestedSorts.TryGetValue(TagSortField.EnglishName, out var englishNameDir))
|
||||||
|
finalSorts.Add(new(TagSortField.EnglishName, englishNameDir));
|
||||||
|
|
||||||
|
if (requestedSorts.TryGetValue(TagSortField.Name, out var nameDir))
|
||||||
|
finalSorts.Add(new(TagSortField.Name, nameDir));
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
if (finalSorts.Count == 0)
|
||||||
|
finalSorts.Add(new(TagSortField.Name, Application.Common.Search.SortDirection.Ascending));
|
||||||
|
|
||||||
|
return finalSorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigateToVoiceWorkSearch(TagSearchItem item)
|
||||||
|
{
|
||||||
|
VoiceWorkFilterState state = new()
|
||||||
|
{
|
||||||
|
TagIds = [item.TagId]
|
||||||
|
};
|
||||||
|
|
||||||
|
//string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Path);
|
||||||
|
string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Authority);
|
||||||
|
string uri = QueryHelpers.AddQueryString($"{basePath}/voiceworks", state.ToQuery());
|
||||||
|
|
||||||
|
NavigationManager.NavigateTo(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetStatus(TagSearchItem item, TagStatus status)
|
||||||
|
{
|
||||||
|
UpdateTagStatusRequest request = new(
|
||||||
|
TagId: item.TagId,
|
||||||
|
TagStatus: status
|
||||||
);
|
);
|
||||||
|
|
||||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
UpdateTagStatusResponse? response = await Client.UpdateTagStatusAsync(request);
|
||||||
var result = await Client.SearchAsync(request);
|
|
||||||
|
|
||||||
searchResults = result?.Results ?? new();
|
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 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IncrementCount(TagSearchItem item)
|
protected override TagFilterState ParseStateFromUri(string absoluteUri)
|
||||||
|
=> TagFilterState.FromQuery(new Uri(absoluteUri).Query);
|
||||||
|
|
||||||
|
protected override string BuildUri(TagFilterState state)
|
||||||
{
|
{
|
||||||
//item.Favorite = !item.Favorite;
|
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>
|
<h3>Voice Works</h3>
|
||||||
|
|
||||||
<VoiceWorkFilters Value="@State" ValueChanged="UpdateAsync" />
|
<VoiceWorkFilters Value="@State" ValueChanged="UpdateAsync" />
|
||||||
<JProductCollection Products="Result?.Items"></JProductCollection>
|
<JProductCollection Products="Result?.Items" ProductDeleted="OnProductDeleted"></JProductCollection>
|
||||||
|
|
||||||
@if (Result is not null)
|
@if (Result is not null)
|
||||||
{
|
{
|
||||||
@@ -69,4 +69,9 @@
|
|||||||
|
|
||||||
protected override Task<SearchResult<VoiceWorkSearchResult>> ExecuteSearchAsync(VoiceWorkFilterState state, CancellationToken ct)
|
protected override Task<SearchResult<VoiceWorkSearchResult>> ExecuteSearchAsync(VoiceWorkFilterState state, CancellationToken ct)
|
||||||
=> Client.SearchAsync(state.ToSearchRequest(), ct).ContinueWith(t => t.Result?.Results ?? new SearchResult<VoiceWorkSearchResult>(), ct);
|
=> Client.SearchAsync(state.ToSearchRequest(), ct).ContinueWith(t => t.Result?.Results ?? new SearchResult<VoiceWorkSearchResult>(), ct);
|
||||||
|
|
||||||
|
private async Task OnProductDeleted()
|
||||||
|
{
|
||||||
|
_ = RunSearchAsync(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,7 @@ builder.Services.AddScoped<SessionState>();
|
|||||||
builder.Services.AddMudServices();
|
builder.Services.AddMudServices();
|
||||||
builder.Services.AddRadzenComponents();
|
builder.Services.AddRadzenComponents();
|
||||||
builder.Services.AddBitBlazorUIServices();
|
builder.Services.AddBitBlazorUIServices();
|
||||||
|
builder.Services.AddAntDesign();
|
||||||
|
|
||||||
builder.Services.AddScoped<VoiceWorksClient>();
|
builder.Services.AddScoped<VoiceWorksClient>();
|
||||||
builder.Services.AddScoped<ILookupDataService, LookupDataService>();
|
builder.Services.AddScoped<ILookupDataService, LookupDataService>();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public interface ILookupDataService
|
|||||||
{
|
{
|
||||||
List<BitDropdownItem<Locale>> GetLocales();
|
List<BitDropdownItem<Locale>> GetLocales();
|
||||||
List<BitDropdownItem<SaleStatus?>> GetSaleStatuses();
|
List<BitDropdownItem<SaleStatus?>> GetSaleStatuses();
|
||||||
|
List<SelectOption<SaleStatus?>> GetSaleStatuses2();
|
||||||
List<BitDropdownItem<CircleStatus?>> GetCircleStatuses();
|
List<BitDropdownItem<CircleStatus?>> GetCircleStatuses();
|
||||||
List<BitDropdownItem<TagStatus?>> GetTagStatuses();
|
List<BitDropdownItem<TagStatus?>> GetTagStatuses();
|
||||||
List<BitDropdownItem<CreatorStatus?>> GetCreatorStatuses();
|
List<BitDropdownItem<CreatorStatus?>> GetCreatorStatuses();
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ public sealed class LookupDataService(VoiceWorksClient client) : ILookupDataServ
|
|||||||
new() { Text = "All", Value = null }
|
new() { Text = "All", Value = null }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public List<SelectOption<SaleStatus?>> GetSaleStatuses2() =>
|
||||||
|
[
|
||||||
|
new() { Label = "Available", Value = SaleStatus.Available },
|
||||||
|
new() { Label = "Upcoming", Value = SaleStatus.Upcoming },
|
||||||
|
new() { Label = "All", Value = null }
|
||||||
|
];
|
||||||
|
|
||||||
public List<BitDropdownItem<CircleStatus?>> GetCircleStatuses() =>
|
public List<BitDropdownItem<CircleStatus?>> GetCircleStatuses() =>
|
||||||
[
|
[
|
||||||
new() { Text = "Not Blacklisted", Value = CircleStatus.NotBlacklisted },
|
new() { Text = "Not Blacklisted", Value = CircleStatus.NotBlacklisted },
|
||||||
@@ -132,3 +139,9 @@ public sealed class LookupDataService(VoiceWorksClient client) : ILookupDataServ
|
|||||||
return _creators;
|
return _creators;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SelectOption<T>
|
||||||
|
{
|
||||||
|
public string? Label { get; set; }
|
||||||
|
public T? Value { get; set; }
|
||||||
|
}
|
||||||
@@ -1,34 +1,93 @@
|
|||||||
using JSMR.Application.Circles.Queries.Search;
|
using JSMR.Application.Circles.Queries.Search;
|
||||||
|
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
|
||||||
using JSMR.Application.Creators.Queries.Search;
|
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.Tags.Queries.Search;
|
||||||
|
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||||
|
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||||
|
using JSMR.UI.Blazor.Exceptions;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace JSMR.UI.Blazor.Services;
|
namespace JSMR.UI.Blazor.Services;
|
||||||
|
|
||||||
public class VoiceWorksClient(HttpClient http)
|
public class VoiceWorksClient(HttpClient http)
|
||||||
{
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new JsonStringEnumConverter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
public async Task<SearchVoiceWorksResponse?> SearchAsync(SearchVoiceWorksRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct);
|
using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct);
|
||||||
return await resp.Content.ReadFromJsonAsync<SearchVoiceWorksResponse>(cancellationToken: ct);
|
return await resp.Content.ReadFromJsonAsync<SearchVoiceWorksResponse>(JsonOptions, cancellationToken: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SearchCirclesResponse?> SearchAsync(SearchCirclesRequest request, CancellationToken ct = default)
|
public async Task<SearchCirclesResponse?> SearchAsync(SearchCirclesRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var resp = await http.PostAsJsonAsync("/api/circles/search", request, ct);
|
using var resp = await http.PostAsJsonAsync("/api/circles/search", request, ct);
|
||||||
return await resp.Content.ReadFromJsonAsync<SearchCirclesResponse>(cancellationToken: ct);
|
return await resp.Content.ReadFromJsonAsync<SearchCirclesResponse>(JsonOptions, cancellationToken: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SearchCreatorsResponse?> SearchAsync(SearchCreatorsRequest request, CancellationToken ct = default)
|
public async Task<SearchCreatorsResponse?> SearchAsync(SearchCreatorsRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var resp = await http.PostAsJsonAsync("/api/creators/search", request, ct);
|
using var resp = await http.PostAsJsonAsync("/api/creators/search", request, ct);
|
||||||
return await resp.Content.ReadFromJsonAsync<SearchCreatorsResponse>(cancellationToken: ct);
|
return await resp.Content.ReadFromJsonAsync<SearchCreatorsResponse>(JsonOptions, cancellationToken: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SearchTagsResponse?> SearchAsync(SearchTagsRequest request, CancellationToken ct = default)
|
public async Task<SearchTagsResponse?> SearchAsync(SearchTagsRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var resp = await http.PostAsJsonAsync("/api/tags/search", request, ct);
|
using var resp = await http.PostAsJsonAsync("/api/tags/search", request, ct);
|
||||||
return await resp.Content.ReadFromJsonAsync<SearchTagsResponse>(cancellationToken: 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();
|
_ = 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)
|
private void NavigateToState(TState next)
|
||||||
{
|
{
|
||||||
string uri = BuildUri(next);
|
string uri = BuildUri(next);
|
||||||
@@ -73,14 +86,17 @@ public abstract class SearchPageBase<TState, TItem> : ComponentBase, IAsyncDispo
|
|||||||
_ = RunSearchAsync();
|
_ = RunSearchAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunSearchAsync()
|
protected async Task RunSearchAsync(bool scrollTotop = true)
|
||||||
{
|
{
|
||||||
|
if (scrollTotop)
|
||||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
||||||
|
|
||||||
|
Console.WriteLine("Got here 2");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
//StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource.Cancel();
|
||||||
_cancellationTokenSource = new();
|
_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);
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -592,6 +592,7 @@ code {
|
|||||||
.j-tags {
|
.j-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .75rem .5rem;
|
gap: .75rem .5rem;
|
||||||
|
gap: .5rem .5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
/*gap: .5rem 1rem;*/
|
/*gap: .5rem 1rem;*/
|
||||||
}
|
}
|
||||||
@@ -647,41 +648,80 @@ code {
|
|||||||
.j-chip {
|
.j-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .4rem;
|
gap: .5em;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
/*--chip-rgb: var(--secondary-rgb, 148 163 184);
|
||||||
|
--chip-tint-alpha: 0.12;*/
|
||||||
|
|
||||||
--chip-rgb: var(--secondary-rgb, 148 163 184);
|
--chip-rgb: var(--secondary-rgb, 148 163 184);
|
||||||
|
--chip-bg-rgb: transparent;
|
||||||
|
--chip-fg-rgb: var(--chip-rgb);
|
||||||
|
--chip-border-rgb: transparent;
|
||||||
--chip-tint-alpha: 0.12;
|
--chip-tint-alpha: 0.12;
|
||||||
|
color: rgb(var(--chip-fg-rgb));
|
||||||
|
background: rgb(var(--chip-bg-rgb));
|
||||||
|
border: 1px solid rgb(var(--chip-border-rgb));
|
||||||
|
padding: .5em 1em;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
.j-chip.is-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
--chip-hover-alpha: 0.2;
|
transition: background .2s linear, color .2s linear, border-color .2s linear, filter .2s linear;
|
||||||
transition: .2s linear;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.j-chip.is-clickable:hover {
|
.j-chip.is-clickable:hover {
|
||||||
background: rgb(var(--chip-rgb) / var(--chip-hover-alpha));
|
filter: brightness(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.j-chip.varient-filled {
|
.j-chip.varient-filled {
|
||||||
padding: .4rem .8rem;
|
--chip-bg-rgb: var(--chip-rgb);
|
||||||
border-radius: 1rem;
|
--chip-border-rgb: var(--chip-rgb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*.j-chip.varient-outlined {
|
||||||
|
--chip-bg-rgb: transparent;
|
||||||
|
--chip-fg-rgb: var(--chip-rgb);
|
||||||
|
--chip-border-rgb: var(--chip-rgb);
|
||||||
|
}*/
|
||||||
|
|
||||||
.j-chip.varient-outlined {
|
.j-chip.varient-outlined {
|
||||||
border-width: 1px;
|
background: rgba(0,0,0,.35);
|
||||||
border-style: solid;
|
border: 1px solid rgb(var(--chip-rgb) / 0.65);
|
||||||
padding: .4rem .8rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
border-color: rgb(var(--chip-rgb));
|
|
||||||
color: rgb(var(--chip-rgb));
|
color: rgb(var(--chip-rgb));
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .j-chip.varient-tint {
|
||||||
|
--chip-bg-rgb: var(--chip-rgb) / var(--chip-tint-alpha);
|
||||||
|
--chip-fg-rgb: var(--chip-rgb);
|
||||||
|
--chip-border-rgb: transparent;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.j-chip.varient-tint {
|
||||||
|
--chip-bg-alpha: 0.14;
|
||||||
|
--chip-border-alpha: 0.32;
|
||||||
|
color: rgb(var(--chip-rgb));
|
||||||
|
background: rgb(var(--chip-rgb) / var(--chip-bg-alpha));
|
||||||
|
/*border: 1px solid rgb(var(--chip-rgb) / var(--chip-border-alpha));*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Old ? */
|
||||||
.j-chip.tone-tint {
|
.j-chip.tone-tint {
|
||||||
background: rgb(var(--chip-rgb) / var(--chip-tint-alpha));
|
background: rgb(var(--chip-rgb) / var(--chip-tint-alpha));
|
||||||
}
|
}
|
||||||
@@ -698,6 +738,94 @@ code {
|
|||||||
--chip-rgb: var(--rgb-teal);
|
--chip-rgb: var(--rgb-teal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* New */
|
||||||
|
.j-chip.color-primary {
|
||||||
|
--chip-rgb: var(--rgb-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-primary {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-primary, 0 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-secondary {
|
||||||
|
--chip-rgb: var(--rgb-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-secondary {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-secondary, 0 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-mint {
|
||||||
|
--chip-rgb: var(--rgb-mint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-mint {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-mint, 0 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-green {
|
||||||
|
--chip-rgb: var(--rgb-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-green {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-green, 255 255 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-yellow {
|
||||||
|
color: rgb(var(--chip-fg-rgb));
|
||||||
|
--chip-rgb: var(--rgb-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-yellow {
|
||||||
|
--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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-pink {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-pink, 255 255 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-teal {
|
||||||
|
--chip-rgb: var(--rgb-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-teal {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-teal, 255 255 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-blue {
|
||||||
|
--chip-rgb: var(--rgb-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-blue {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-blue, 255 255 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-red {
|
||||||
|
--chip-rgb: var(--rgb-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-red {
|
||||||
|
--chip-fg-rgb: var(--rgb-on-red, 255 255 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.color-black {
|
||||||
|
color: inherit;
|
||||||
|
--chip-rgb: 39 39 39;
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-chip.varient-filled.color-black {
|
||||||
|
--chip-fg-rgb: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* Icons */
|
/* Icons */
|
||||||
.j-icon {
|
.j-icon {
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
@@ -825,6 +953,30 @@ code {
|
|||||||
mask-image: url("../svg/age-rating.svg");
|
mask-image: url("../svg/age-rating.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.j-icon-download {
|
||||||
|
mask-image: url("../svg/cloud-download.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-icon-download-fill {
|
||||||
|
mask-image: url("../svg/cloud-download-fill.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-icon-microphone {
|
||||||
|
mask-image: url("../svg/microphone.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.j-icon-microphone-fill {
|
||||||
|
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 {
|
.j-icon-2 {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|||||||
@@ -40,9 +40,18 @@
|
|||||||
--input-focus-border-color: #64b5f6;
|
--input-focus-border-color: #64b5f6;
|
||||||
--input-focus-box-shadow: 0 0 0 1px #93cbf9;
|
--input-focus-box-shadow: 0 0 0 1px #93cbf9;
|
||||||
/* RGB Tokens */
|
/* RGB Tokens */
|
||||||
|
--rgb-primary: 180 200 214;
|
||||||
|
--rgb-secondary: 200 220 234;
|
||||||
--rgb-mint: 167 243 208;
|
--rgb-mint: 167 243 208;
|
||||||
--rgb-green: 175 224 125;
|
--rgb-green: 175 224 125;
|
||||||
--rgb-teal: 110 236 255;
|
--rgb-teal: 110 236 255;
|
||||||
|
--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;
|
||||||
/* Colors */
|
/* Colors */
|
||||||
--color-primary: rgb(180,200, 214);
|
--color-primary: rgb(180,200, 214);
|
||||||
--color-secondary: rgb(200,220,234);
|
--color-secondary: rgb(200,220,234);
|
||||||
@@ -55,6 +64,7 @@
|
|||||||
--color-orange: #ffa773;
|
--color-orange: #ffa773;
|
||||||
--color-pink: #e06894;
|
--color-pink: #e06894;
|
||||||
--color-dark-pink: #832044;
|
--color-dark-pink: #832044;
|
||||||
|
--color-red: #e06868;
|
||||||
/* Background Colors */
|
/* Background Colors */
|
||||||
--background-color-primary: rgb(57, 79, 94);
|
--background-color-primary: rgb(57, 79, 94);
|
||||||
--background-color-secondary: rgb(30, 53, 69);
|
--background-color-secondary: rgb(30, 53, 69);
|
||||||
@@ -65,6 +75,47 @@
|
|||||||
--surface-container-outline-high: rgb(83, 99, 109);
|
--surface-container-outline-high: rgb(83, 99, 109);
|
||||||
--surface-container-outline: rgb(72, 88, 99);
|
--surface-container-outline: rgb(72, 88, 99);
|
||||||
--surface-container-outline-low: rgb(63, 78, 88);
|
--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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user