1
0
mirror of synced 2024-11-23 22:10:59 +01:00

Add Play Records page and apis, finish migration (except for online matching)

This commit is contained in:
asesidaa 2023-02-23 00:35:59 +08:00
parent e5eee66ff2
commit eb4380b6d5
28 changed files with 450 additions and 50 deletions

View File

@ -0,0 +1,125 @@
using System.Diagnostics.CodeAnalysis;
using Domain.Entities;
using Domain.Enums;
namespace Application.Api;
public record GetSongPlayRecordsQuery(long cardId) : IRequestWrapper<List<SongPlayRecord>>;
public class GetSongPlayRecordsQueryHandler : RequestHandlerBase<GetSongPlayRecordsQuery, List<SongPlayRecord>>
{
public GetSongPlayRecordsQueryHandler(ICardDependencyAggregate aggregate) : base(aggregate)
{
}
[SuppressMessage("ReSharper.DPA", "DPA0007: Large number of DB records")]
public override async Task<ServiceResult<List<SongPlayRecord>>> Handle(GetSongPlayRecordsQuery request,
CancellationToken cancellationToken)
{
var exists = await CardDbContext.CardDetails.AnyAsync(detail => detail.CardId == request.cardId);
if (!exists)
{
return ServiceResult.Failed<List<SongPlayRecord>>(ServiceError.CustomMessage("No play record"));
}
var results = new List<SongPlayRecord>();
var musics = await MusicDbContext.MusicUnlocks.ToDictionaryAsync(unlock => unlock.MusicId, cancellationToken);
var playCounts = await CardDbContext.CardDetails
.Where(detail => detail.CardId == request.cardId &&
detail.Pcol1 == 20)
.Select(detail => new
{
MusicId = detail.Pcol2,
Difficulty = (Difficulty)detail.Pcol3,
Detail = detail
})
.ToDictionaryAsync(arg => new { arg.MusicId, arg.Difficulty }, cancellationToken: cancellationToken);
var stageDetails = await CardDbContext.CardDetails
.Where(detail => detail.CardId == request.cardId &&
detail.Pcol1 == 21)
.Select(detail => new
{
MusicId = detail.Pcol2,
Difficulty = (Difficulty)detail.Pcol3,
Score = detail.ScoreUi1,
MaxChain = detail.ScoreUi3,
})
.ToDictionaryAsync(arg => new { arg.MusicId, arg.Difficulty }, cancellationToken);
var favorites = await CardDbContext.CardDetails
.Where(detail => detail.CardId == request.cardId &&
detail.Pcol1 == 10)
.Select(detail => new { MusicId = detail.Pcol2, IsFavorite = detail.Fcol1 == 1 })
.ToListAsync(cancellationToken);
foreach (var song in favorites)
{
var musicId = song.MusicId;
var music = musics.GetValueOrDefault(musicId);
var songPlayRecord = new SongPlayRecord
{
MusicId = (int)musicId,
Title = music?.Title ?? string.Empty,
Artist = music?.Artist ?? string.Empty,
IsFavorite = song.IsFavorite
};
foreach (var difficulty in DifficultyExtensions.GetValues())
{
var key = new { MusicId = musicId, Difficulty = difficulty };
if (!playCounts.ContainsKey(key) || !stageDetails.ContainsKey(key)) continue;
var playCountDetail = playCounts[key].Detail;
var playCount = playCountDetail.ScoreUi1;
var stageDetail = stageDetails[key];
var score = stageDetail.Score;
var maxChain = stageDetail.MaxChain;
var clearState = GetClearState(playCountDetail);
var stagePlayRecord = new StagePlayRecord
{
Difficulty = difficulty,
ClearState = clearState,
PlayCount = (int)playCount,
Score = (int)score,
MaxChain = (int)maxChain,
LastPlayTime = playCountDetail?.LastPlayTime ?? DateTime.MinValue
};
songPlayRecord.StagePlayRecords.Add(stagePlayRecord);
}
songPlayRecord.TotalPlayCount = songPlayRecord.StagePlayRecords.Sum(record => record.PlayCount);
if (songPlayRecord.StagePlayRecords.Count > 0)
{
results.Add(songPlayRecord);
}
}
return new ServiceResult<List<SongPlayRecord>>(results);
}
private static ClearState GetClearState(CardDetail detail)
{
var result = ClearState.Failed;
if (detail.ScoreUi2 > 0)
{
result = ClearState.Clear;
}
if (detail.ScoreUi3 > 0)
{
result = ClearState.NoMiss;
}
if (detail.ScoreUi4 > 0)
{
result = ClearState.FullChain;
}
if (detail.ScoreUi6 > 0)
{
result = ClearState.Perfect;
}
return result;
}
}

View File

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta06" PrivateAssets="all" ExcludeAssets="runtime"/>
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta06" PrivateAssets="all" ExcludeAssets="runtime" />
</ItemGroup>
</Project>

View File

@ -34,5 +34,5 @@ public partial class CardDetail
public long Fcol3 { get; set; }
public DateTime LastPlayTime { get; set; }
public DateTime? LastPlayTime { get; set; }
}

View File

@ -0,0 +1,14 @@
using NetEscapades.EnumGenerators;
namespace Domain.Enums;
[EnumExtensions]
public enum ClearState
{
NotPlayed = 0,
Failed,
Clear,
NoMiss,
FullChain,
Perfect
}

View File

@ -0,0 +1,12 @@
using NetEscapades.EnumGenerators;
namespace Domain.Enums;
[EnumExtensions]
public enum Difficulty
{
Simple = 0,
Normal = 1,
Hard = 2,
Extra = 3
}

View File

@ -48,7 +48,7 @@ namespace Infrastructure.Migrations
fcol1 = table.Column<long>(type: "INTEGER", nullable: false),
fcol2 = table.Column<long>(type: "INTEGER", nullable: false),
fcol3 = table.Column<long>(type: "INTEGER", nullable: false),
lastplaytime = table.Column<long>(name: "last_play_time", type: "INTEGER", nullable: false)
lastplaytime = table.Column<long>(name: "last_play_time", type: "INTEGER", nullable: true)
},
constraints: table =>
{

View File

@ -76,7 +76,7 @@ public partial class CardDbContext : DbContext, ICardDbContext
entity.Property(e => e.Fcol3).HasColumnName("fcol3");
entity.Property(e => e.LastPlayTenpoId).HasColumnName("last_play_tenpo_id").IsRequired(false);
entity.Property(e => e.LastPlayTime).HasColumnName("last_play_time")
.HasConversion<DateTimeToTicksConverter>();
.HasConversion<DateTimeToTicksConverter>().IsRequired(false);
entity.Property(e => e.ScoreBi1).HasColumnName("score_bi1");
entity.Property(e => e.ScoreI1).HasColumnName("score_i1");
entity.Property(e => e.ScoreUi1).HasColumnName("score_ui1");

View File

@ -1,4 +1,4 @@
{
"CardDbName": "card.db3",
"CardDbName": "card.full.db3",
"MusicDbName": "music471omni.db3"
}

View File

@ -23,7 +23,15 @@ public class ProfilesController : BaseController<ProfilesController>
return result;
}
[HttpPost("Favorite")]
[HttpGet("SongPlayRecords/{cardId:long}")]
public async Task<ServiceResult<List<SongPlayRecord>>> GetSongPlayRecords(long cardId)
{
var result = await Mediator.Send(new GetSongPlayRecordsQuery(cardId));
return result;
}
[HttpPost("SetFavorite")]
public async Task<ServiceResult<bool>> SetFavoriteMusic(MusicFavoriteDto favorite)
{
var result = await Mediator.Send(new SetFavoriteMusicCommand(favorite));

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Shared.Models;
public class SongPlayRecord
{
public string Title { get; set; } = string.Empty;
public string Artist { get; set; } = string.Empty;
public int MusicId { get; set; }
public bool IsFavorite { get; set; }
public int TotalPlayCount { get; set; }
public List<StagePlayRecord> StagePlayRecords { get; set; } = new();
}

View File

@ -0,0 +1,18 @@
using Domain.Enums;
namespace Shared.Models;
public class StagePlayRecord
{
public int Score { get; set; }
public int PlayCount { get; set; }
public int MaxChain { get; set; }
public Difficulty Difficulty { get; set; }
public ClearState ClearState { get; set; }
public DateTime LastPlayTime { get; set; }
}

View File

@ -1,3 +1,6 @@
// Global using directives
global using MudBlazor;
global using System.Net.Http.Json;
global using Microsoft.AspNetCore.Components;
global using MudBlazor;
global using Shared.Models;

View File

@ -73,7 +73,7 @@
AnchorOrigin="Origin.BottomCenter"
TransformOrigin="Origin.TopCenter">
<MudMenuItem Href="@($"Cards/TotalResult/{card.CardId}")">Total Result</MudMenuItem>
<MudMenuItem Href="@($"Cards/Results/{card.CardId}")">Song Play Results</MudMenuItem>
<MudMenuItem Href="@($"Cards/PlayRecords/{card.CardId}")">Song Play Results</MudMenuItem>
</MudMenu>
</MudStack>
</MudCardActions>

View File

@ -1,7 +1,4 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components;
using Shared.Dto.Api;
using Shared.Models;
using Shared.Dto.Api;
using Throw;
using WebUI.Pages.Dialogs;

View File

@ -1,7 +1,4 @@
@using WebUI.Services
@using Shared.Models
@using WebUI.Common.Models
@inject IDataService DataService
@inject IDataService DataService
<MudDialog>
<DialogContent>

View File

@ -1,7 +1,4 @@
@using WebUI.Services
@using Shared.Models
@using WebUI.Common.Models
@inject IDataService DataService
@inject IDataService DataService
<MudDialog>
<DialogContent>

View File

@ -1,6 +1,5 @@
@using System.Text.RegularExpressions
@using Shared.Dto.Api
@using Shared.Models
@using Throw
@inject HttpClient Client
@inject ILogger<ChangePlayerNameDialog> Logger

View File

@ -1,7 +1,4 @@
@using WebUI.Services
@using Shared.Models
@using WebUI.Common.Models
@inject IDataService DataService
@inject IDataService DataService
<MudDialog>
<DialogContent>

View File

@ -0,0 +1,61 @@
@using Shared.Dto.Api
@using Throw
@inject HttpClient Client
<MudDialog>
<TitleContent>
@if (!Data.IsFavorite)
{
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.BookmarkAdd" Color="Color.Secondary"/>
Add to favorite?
</MudText>
}
else
{
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.BookmarkRemove" Color="Color.Secondary"/>
Remove from favorite?
</MudText>
}
</TitleContent>
<DialogContent>
<MudTextField Value="@Data.Title" Label="Song Title" ReadOnly="true"/>
<MudTextField Value="@Data.Artist" Label="Artist Name" ReadOnly="true"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Confirm</MudButton>
</DialogActions>
</MudDialog>
@code{
[CascadingParameter]
public required MudDialogInstance MudDialog { get; set; }
[Parameter]
public required SongPlayRecord Data { get; set; }
[Parameter]
public long CardId { get; set; }
private async Task Submit()
{
var favoriteData = new MusicFavoriteDto
{
CardId = CardId,
IsFavorite = !Data.IsFavorite,
MusicId = Data.MusicId
};
var response = await Client.PostAsJsonAsync("api/Profiles/SetFavorite", favoriteData);
var result = await response.Content.ReadFromJsonAsync<ServiceResult<bool>>();
result.ThrowIfNull();
MudDialog.Close(DialogResult.Ok(result));
}
private void Cancel() => MudDialog.Cancel();
}

View File

@ -1,7 +1,4 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components;
using Shared.Models;
using Throw;
using Throw;
using WebUI.Pages.Dialogs;
using WebUI.Services;

View File

@ -0,0 +1,84 @@
@page "/Cards/PlayRecords/{cardId:long}"
<MudBreadcrumbs Items="breadcrumbs" Class="px-0"></MudBreadcrumbs>
<PageTitle>Song Play Records</PageTitle>
<h1>Song Play Records</h1>
@if (errorMessage is not null)
{
<MudText Color="Color.Error" Typo="Typo.h3">@errorMessage</MudText>
return;
}
@if (songPlayRecords is null)
{
<MudStack>
<MudSkeleton Width="100%"/>
<MudSkeleton Width="100%"/>
<MudSkeleton Width="100%"/>
<MudSkeleton Width="100%"/>
<MudSkeleton Width="100%"/>
<MudSkeleton Width="100%"/>
</MudStack>
return;
}
<MudDataGrid T="SongPlayRecord"
Items="songPlayRecords"
SortMode="SortMode.Single">
<ToolBarContent>
<MudText Typo="Typo.h6">Played Songs</MudText>
</ToolBarContent>
<Columns>
<HierarchyColumn T="SongPlayRecord"></HierarchyColumn>
<Column T="SongPlayRecord" Field="@nameof(SongPlayRecord.Title)" Title="Song Title"/>
<Column T="SongPlayRecord" Field="@nameof(SongPlayRecord.Artist)" Title="Artist"/>
<Column T="SongPlayRecord" Field="@nameof(SongPlayRecord.TotalPlayCount)" Title="Total Play Count"/>
<Column T="SongPlayRecord" Field="@nameof(SongPlayRecord.IsFavorite)" Title="Is Favorite">
<CellTemplate>
<MudToggleIconButton Toggled="@context.Item.IsFavorite"
ToggledChanged="@(() => OnFavoriteToggled(context.Item))"
Icon="@Icons.Material.Filled.FavoriteBorder"
Color="@Color.Secondary"
Title="Add to favorite"
ToggledIcon="@Icons.Material.Filled.Favorite"
ToggledColor="@Color.Secondary"
ToggledTitle="Remove from favorite"/>
</CellTemplate>
</Column>
</Columns>
<ChildRowContent>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.body1">Song Play Details</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudTable Items="@context.Item.StagePlayRecords">
<HeaderContent>
<MudTh>Difficulty</MudTh>
<MudTh>Play Count</MudTh>
<MudTh>Clear State</MudTh>
<MudTh>Score</MudTh>
<MudTh>Rating</MudTh>
<MudTh>Max Chain</MudTh>
<MudTh>Last Play Time</MudTh>
</HeaderContent>
<RowTemplate Context="stage">
<MudTd DataLabel="Difficulty">@stage.Difficulty</MudTd>
<MudTd DataLabel="PlayCount">@stage.PlayCount</MudTd>
<MudTd DataLabel="ClearState">@stage.ClearState</MudTd>
<MudTd DataLabel="Score">@stage.Score</MudTd>
<MudTd DataLabel="Rating">@GetRating(stage.Score)</MudTd>
<MudTd DataLabel="MaxChain">@stage.MaxChain</MudTd>
<MudTd DataLabel="LastPlayTime">@stage.LastPlayTime</MudTd>
</RowTemplate>
</MudTable>
</MudCardContent>
</MudCard>
</ChildRowContent>
<PagerContent>
<MudDataGridPager T="SongPlayRecord"/>
</PagerContent>
</MudDataGrid>

View File

@ -0,0 +1,85 @@
using Throw;
using WebUI.Pages.Dialogs;
namespace WebUI.Pages;
public partial class PlayRecords
{
private readonly List<BreadcrumbItem> breadcrumbs = new()
{
new BreadcrumbItem("Cards", href: "/Cards")
};
[Parameter]
public long CardId { get; set; }
[Inject]
public required HttpClient Client { get; set; }
[Inject]
public required IDialogService DialogService { get; set; }
private string? errorMessage;
private List<SongPlayRecord>? songPlayRecords;
protected async override Task OnInitializedAsync()
{
await base.OnInitializedAsync();
breadcrumbs.Add(new BreadcrumbItem($"Card: {CardId}", href:null, disabled:true));
breadcrumbs.Add(new BreadcrumbItem("TotalResult", href: $"/Cards/PlayRecords/{CardId}", disabled: false));
var result = await Client.GetFromJsonAsync<ServiceResult<List<SongPlayRecord>>>($"api/Profiles/SongPlayRecords/{CardId}");
result.ThrowIfNull();
if (!result.Succeeded)
{
errorMessage = result.Error!.Message;
return;
}
songPlayRecords = result.Data;
}
private static string GetRating(int score) => score switch
{
> 990000 => "S++",
> 950000 => "S+",
> 900000 => "S",
> 800000 => "A",
> 700000 => "B",
> 500000 => "C",
> 300000 => "D",
_ => "E"
};
private async Task OnFavoriteToggled(SongPlayRecord data)
{
var options = new DialogOptions
{
CloseOnEscapeKey = false,
DisableBackdropClick = true,
FullWidth = true
};
var parameters = new DialogParameters
{
{ "Data", data },
{"CardId", CardId}
};
var dialog = await DialogService.ShowAsync<FavoriteDialog>("Favorite", parameters, options);
var result = await dialog.Result;
if (result.Canceled)
{
return;
}
if (result.Data is ServiceResult<bool> serviceResult && serviceResult.Data)
{
data.IsFavorite = !data.IsFavorite;
}
}
}

View File

@ -35,6 +35,7 @@
<MudList>
<MudListSubheader>Player Name: @totalResultData.PlayerName</MudListSubheader>
<MudListItem>Total Score: @totalResultData.PlayerData.TotalScore</MudListItem>
<MudListItem>Rank: @totalResultData.PlayerData.Rank</MudListItem>
<MudListItem>Average Score: @totalResultData.PlayerData.AverageScore</MudListItem>
<MudListItem>Played Song Count: @totalResultData.PlayerData.PlayedSongCount / @totalResultData.PlayerData.TotalSongCount</MudListItem>
<MudListItem>Cleared Stage Count: @totalResultData.StageCountData.Cleared / @totalResultData.StageCountData.Total</MudListItem>

View File

@ -1,7 +1,4 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components;
using Shared.Models;
using Throw;
using Throw;
namespace WebUI.Pages;

View File

@ -1,7 +1,4 @@
using System.Collections.ObjectModel;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Throw;
 using Throw;
using WebUI.Common.Models;
namespace WebUI.Services;
@ -29,17 +26,17 @@ public class DataService : IDataService
public async Task InitializeAsync()
{
var avatarList = await client.GetFromJsonAsync("data/Avatars.json", SourceGenerationContext.Default.ListAvatar);
var avatarList = await client.GetFromJsonAsync<List<Avatar>>("data/Avatars.json");
avatarList.ThrowIfNull();
avatars = avatarList.ToDictionary(avatar => avatar.AvatarId);
sortedAvatarList = avatarList.OrderBy(avatar => avatar.AvatarId).ToList();
var navigatorList = await client.GetFromJsonAsync("data/Navigators.json", SourceGenerationContext.Default.ListNavigator);
var navigatorList = await client.GetFromJsonAsync<List<Navigator>>("data/Navigators.json");
navigatorList.ThrowIfNull();
navigators = navigatorList.ToDictionary(navigator => navigator.Id);
sortedNavigatorList = navigatorList.OrderBy(navigator => navigator.Id).ToList();
var titleList = await client.GetFromJsonAsync("data/Titles.json", SourceGenerationContext.Default.ListTitle);
var titleList = await client.GetFromJsonAsync<List<Title>>("data/Titles.json");
titleList.ThrowIfNull();
titles = titleList.ToDictionary(title => title.Id);
sortedTitleList = titleList.OrderBy(title => title.Id).ToList();
@ -74,12 +71,4 @@ public class DataService : IDataService
{
return navigators.GetValueOrDefault(id);
}
}
[JsonSerializable(typeof(List<Avatar>))]
[JsonSerializable(typeof(List<Navigator>))]
[JsonSerializable(typeof(List<Title>))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}

View File

@ -1,5 +1,4 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@ -8,4 +7,6 @@
@using Microsoft.JSInterop
@using MudBlazor
@using WebUI
@using WebUI.Common
@using WebUI.Common
@using WebUI.Services
@using WebUI.Common.Models