1
0
mirror of synced 2024-11-12 00:40:51 +01:00

Add last play time support

Support setting avatar, navigator and titles on web interface (unoptimized)
This commit is contained in:
jiych1 2022-07-06 00:52:38 +08:00
parent 95d9d0be5f
commit 4cd290aedc
24 changed files with 55725 additions and 350 deletions

View File

@ -1,3 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Adlibs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Keynum/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Keynum/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unlockable/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Touhou/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unlockable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vocaloid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -126,7 +126,8 @@ public static class Configs
public const string RANK_STATUS_XPATH = $"{ROOT_XPATH}/ranking_status"; public const string RANK_STATUS_XPATH = $"{ROOT_XPATH}/ranking_status";
public const int CONFIG_PCOL1 = 0; public const int FIRST_CONFIG_PCOL1 = 0;
public const int SECOND_CONFIG_PCOL1 = 1;
public const int CONFIG_PCOL2 = 0; public const int CONFIG_PCOL2 = 0;
public const int CONFIG_PCOL3 = 0; public const int CONFIG_PCOL3 = 0;

View File

@ -65,26 +65,38 @@ public class ApiController : WebApiController
// ReSharper disable once UnusedMember.Global // ReSharper disable once UnusedMember.Global
public bool SetPlayOption([JsonData] PlayOption data) public bool SetPlayOption([JsonData] PlayOption data)
{ {
var existing = cardSqLiteConnection.Table<CardDetail>() var firstConfig = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == data.CardId .Where(detail => detail.CardId == data.CardId
&& detail.Pcol1 == Configs.CONFIG_PCOL1 && detail.Pcol1 == Configs.FIRST_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3);
var secondConfig = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == data.CardId
&& detail.Pcol1 == Configs.SECOND_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2 && detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3); && detail.Pcol3 == Configs.CONFIG_PCOL3);
if (!existing.Any()) if (!firstConfig.Any() || !secondConfig.Any())
{ {
$"Trying to update non existing card's config! Card id {data.CardId}".Warn(); $"Trying to update non existing card's config! Card id {data.CardId}".Warn();
return false; return false;
} }
var cardDetail = existing.First(); var firstDetail = firstConfig.First();
cardDetail.ScoreUi1 = (long)data.FastSlowIndicator; firstDetail.ScoreUi1 = (long)data.FastSlowIndicator;
cardDetail.ScoreUi2 = (long)data.FeverTrance; firstDetail.ScoreUi2 = (long)data.FeverTrance;
firstDetail.ScoreI1 = data.AvatarId;
var result = cardSqLiteConnection.Update(cardDetail); firstDetail.Fcol2 = (int)data.TitleId;
return result == 1; var secondDetail = secondConfig.First();
secondDetail.ScoreI1 = data.NavigatorId;
var firstResult = cardSqLiteConnection.Update(firstDetail);
var secondResult = cardSqLiteConnection.Update(secondDetail);
return firstResult == 1 && secondResult == 1;
} }
[Route(HttpVerbs.Get, "/UserDetail/{cardId}")] [Route(HttpVerbs.Get, "/UserDetail/{cardId}")]
@ -125,16 +137,26 @@ public class ApiController : WebApiController
private void ProcessCardDetail(UserDetail userDetail, IDictionary<int, SongPlayData> songPlayDataDict) private void ProcessCardDetail(UserDetail userDetail, IDictionary<int, SongPlayData> songPlayDataDict)
{ {
var option = cardSqLiteConnection.Table<CardDetail>() var firstOption = cardSqLiteConnection.Table<CardDetail>()
.FirstOrDefault(detail => detail.CardId == userDetail.CardId .FirstOrDefault(detail => detail.CardId == userDetail.CardId
&& detail.Pcol1 == Configs.CONFIG_PCOL1 && detail.Pcol1 == Configs.FIRST_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2 && detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3 && detail.Pcol3 == Configs.CONFIG_PCOL3
, new CardDetail , new CardDetail
{ {
CardId = userDetail.CardId CardId = userDetail.CardId
}); });
SetOptions(option, userDetail); var secondOption = cardSqLiteConnection.Table<CardDetail>()
.FirstOrDefault(detail => detail.CardId == userDetail.CardId
&& detail.Pcol1 == Configs.SECOND_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3
, new CardDetail
{
CardId = userDetail.CardId
});
SetOptions(firstOption, secondOption, userDetail);
var songCounts = cardSqLiteConnection.Table<CardDetail>() var songCounts = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == userDetail.CardId && detail.Pcol1 == Configs.COUNT_PCOL1); .Where(detail => detail.CardId == userDetail.CardId && detail.Pcol1 == Configs.COUNT_PCOL1);
@ -162,10 +184,10 @@ public class ApiController : WebApiController
} }
} }
private static void SetOptions(CardDetail cardDetail, UserDetail userDetail) private static void SetOptions(CardDetail firstOptionCardDetail, CardDetail secondOptionCardDetail, UserDetail userDetail)
{ {
var fastSlow = (int)cardDetail.ScoreUi1; var fastSlow = (int)firstOptionCardDetail.ScoreUi1;
var feverTrance = (int)cardDetail.ScoreUi2; var feverTrance = (int)firstOptionCardDetail.ScoreUi2;
if (!Enum.IsDefined(typeof(PlayOptions.FastSlowIndicator), fastSlow)) if (!Enum.IsDefined(typeof(PlayOptions.FastSlowIndicator), fastSlow))
{ {
@ -179,9 +201,12 @@ public class ApiController : WebApiController
userDetail.PlayOption = new PlayOption userDetail.PlayOption = new PlayOption
{ {
CardId = cardDetail.CardId, CardId = firstOptionCardDetail.CardId,
FastSlowIndicator = (PlayOptions.FastSlowIndicator)fastSlow, FastSlowIndicator = (PlayOptions.FastSlowIndicator)fastSlow,
FeverTrance = (PlayOptions.FeverTranceShow)feverTrance FeverTrance = (PlayOptions.FeverTranceShow)feverTrance,
AvatarId = firstOptionCardDetail.ScoreI1,
TitleId = firstOptionCardDetail.Fcol2,
NavigatorId = secondOptionCardDetail.ScoreI1
}; };
} }
private void SetDetails(CardDetail cardDetail, IDictionary<int, SongPlayData> songPlayDataDict, private void SetDetails(CardDetail cardDetail, IDictionary<int, SongPlayData> songPlayDataDict,
@ -241,6 +266,7 @@ public class ApiController : WebApiController
} }
songPlayDetailData.PlayCount = (int)cardDetail.ScoreUi1; songPlayDetailData.PlayCount = (int)cardDetail.ScoreUi1;
songPlayDetailData.LastPlayTime = cardDetail.LastPlayTime;
songPlayDetailData.ClearState = ClearState.Failed; songPlayDetailData.ClearState = ClearState.Failed;
userDetail.PlayedStageCount++; userDetail.PlayedStageCount++;

View File

@ -40,14 +40,14 @@ public class CardServiceController : WebApiController
{ {
if (!Enum.IsDefined(typeof(Command), cmdType)) if (!Enum.IsDefined(typeof(Command), cmdType))
{ {
throw new ArgumentOutOfRangeException(nameof(cmdType), cmdType, "Cmd type is unknown!"); throw new ArgumentOutOfRangeException(nameof(cmdType), cmdType, $"Cmd type is unknown!\n Data is {xmlData}");
} }
var command = (Command)cmdType; var command = (Command)cmdType;
return command switch return command switch
{ {
Command.CardRequest => ProcessCardRequest(mac, cardId, xmlData, type), Command.CardReadRequest or Command.CardWriteRequest => ProcessCardRequest(mac, cardId, xmlData, type),
Command.ReissueRequest => ProcessReissueRequest(), Command.ReissueRequest => ProcessReissueRequest(),
Command.RegisterRequest => ProcessRegisterRequest(cardId, xmlData), Command.RegisterRequest => ProcessRegisterRequest(cardId, xmlData),
_ => throw new ArgumentOutOfRangeException(nameof(command), command, "Command unknown, should never happen!") _ => throw new ArgumentOutOfRangeException(nameof(command), command, "Command unknown, should never happen!")
@ -66,7 +66,7 @@ public class CardServiceController : WebApiController
"Get reissue request, returning not reissue".Info(); "Get reissue request, returning not reissue".Info();
return ConstructResponse("", ReturnCode.NotReissue); return ConstructResponse("", ReturnCode.NotReissue);
} }
private string ProcessCardRequest(string mac, long cardId, string xmlData, int type) private string ProcessCardRequest(string mac, long cardId, string xmlData, int type)
{ {
if (!Enum.IsDefined(typeof(CardRequestType), type)) if (!Enum.IsDefined(typeof(CardRequestType), type))
@ -534,35 +534,56 @@ public class CardServiceController : WebApiController
$"Updated card play count, current count is {data.PlayCount}".Info(); $"Updated card play count, current count is {data.PlayCount}".Info();
} }
private void WriteCardDetail(long cardId, string xmlData) private void WriteCardDetail(long cardId, string xmlData)
{ {
var result = cardSqLiteConnection.Table<CardDetail>() var result = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == cardId); .Where(detail => detail.CardId == cardId);
// Unlock all unlockable songs in card details table when write for the first time // Unlock all unlockable songs in card details table when write card detail for the first time
if (!result.Any()) if (!result.Any())
{ {
var unlockableSongIds = Configs.SETTINGS.UnlockableSongIds; UnlockSongs(cardId);
if (unlockableSongIds is null)
{
unlockableSongIds = Configs.DEFAULT_UNLOCKABLE_SONGS;
}
var detailList = unlockableSongIds.Select(id => new CardDetail
{
CardId = cardId,
Pcol1 = 10,
Pcol2 = id,
Pcol3 = 0,
ScoreUi2 = 1,
ScoreUi6 = 1
})
.ToList();
cardSqLiteConnection.InsertOrIgnoreAll(detailList);
} }
Write<CardDetail>(cardId, xmlData); var reader = new ChoXmlReader<CardDetail>(new StringReader(xmlData)).WithXPath(Configs.DATA_XPATH);
var cardDetail = reader.Read();
if (cardDetail is null)
{
throw new HttpRequestException("Write object is null");
}
cardDetail.SetCardId(cardId);
cardDetail.LastPlayTime = DateTime.Now;
var rowsAffected = cardSqLiteConnection.InsertOrReplace(cardDetail);
if (rowsAffected == 0)
{
throw new ApplicationException("Update database failed!");
}
"Updated card detail".Info();
}
private void UnlockSongs(long cardId)
{
var unlockableSongIds = Configs.SETTINGS.UnlockableSongIds;
if (unlockableSongIds is null)
{
unlockableSongIds = Configs.DEFAULT_UNLOCKABLE_SONGS;
}
var detailList = unlockableSongIds.Select(id => new CardDetail
{
CardId = cardId,
Pcol1 = 10,
Pcol2 = id,
Pcol3 = 0,
ScoreUi2 = 1,
ScoreUi6 = 1,
LastPlayTime = DateTime.Now
})
.ToList();
cardSqLiteConnection.InsertOrIgnoreAll(detailList);
} }
#endregion #endregion
@ -612,7 +633,8 @@ public class CardServiceController : WebApiController
private enum Command private enum Command
{ {
CardRequest = 256, CardReadRequest = 256,
CardWriteRequest = 768,
RegisterRequest = 512, RegisterRequest = 512,
ReissueRequest = 1536 ReissueRequest = 1536
} }

View File

@ -90,6 +90,10 @@ public class CardDetail : Record, ICardIdModel
[ChoXmlElementRecordField(FieldName = "fcol3")] [ChoXmlElementRecordField(FieldName = "fcol3")]
[XmlElement("fcol3")] [XmlElement("fcol3")]
public int Fcol3 { get; set; } public int Fcol3 { get; set; }
[Column("last_play_time")]
[XmlIgnore]
public DateTime LastPlayTime { get; set; } = DateTime.MinValue;
public void SetCardId(long cardId) public void SetCardId(long cardId)
{ {

View File

@ -36,5 +36,9 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Utils" />
</ItemGroup>
</Project> </Project>

View File

@ -1,209 +1,206 @@
@page "/user/{CardId:long}" @page "/user/{CardId:long}"
@using SharedProject.models @using SharedProject.models
@using SharedProject.enums @using SharedProject.enums
@using SharedProject.common
@inject HttpClient Client
@inject IDialogService DialogService
@inject ILogger<User> Logger
<PageTitle>User</PageTitle> <PageTitle>User</PageTitle>
<MudContainer> <MudContainer>
@if (userDetail == null) @if (pageLoading)
{
<MudSkeleton Width="1184px" Height="57px"/>
<MudSkeleton Width="1184px" Height="57px"/>
<MudSkeleton Width="1184px" Height="57px"/>
}
else
{
<MudExpansionPanels>
<MudExpansionPanel Text="Total Result">
<MudList>
<MudListSubheader>Player Name: @userDetail.PlayerName</MudListSubheader>
<MudListItem>Total Score: @userDetail.TotalScore</MudListItem>
<MudListItem>Average Score: @userDetail.AverageScore</MudListItem>
<MudListItem>Played Song Count: @userDetail.PlayedSongCount / @userDetail.TotalSongCount</MudListItem>
<MudListItem>Cleared Stage Count: @userDetail.ClearedStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>No Miss Stage Count: @userDetail.NoMissStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>Full Chain Stage Count: @userDetail.FullChainStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>Perfect Stage Count: @userDetail.PerfectStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S and Above Stage Count: @userDetail.SAboveStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S+ and Above Stage Count: @userDetail.SPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S++ and Above Stage Count: @userDetail.SPlusPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
</MudList>
</MudExpansionPanel>
<MudExpansionPanel Text="PlayOptions">
<MudSelect @bind-Value="@fastSlowIndicator" Label="FAST/SLOW show setting">
@foreach (var item in Enum.GetValues<PlayOptions.FastSlowIndicator>())
{
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@feverTranceShow" Label="FEVER/TRANCE show setting">
@foreach (var item in Enum.GetValues<PlayOptions.FeverTranceShow>())
{
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem>
}
</MudSelect>
<MudButton Disabled="@isSavingOptions" OnClick="SaveOptions" Variant="Variant.Filled" Color="Color.Info">
@if (isSavingOptions)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/>
<MudText Class="ms-2">Saving...</MudText>
}
else
{
<MudIcon Icon="@Icons.Filled.Save"></MudIcon>
<MudText>Save</MudText>
}
</MudButton>
</MudExpansionPanel>
<MudExpansionPanel Text="SongPlayData">
<MudDataGrid T="SongPlayData" Items="@songPlayDataList" Sortable="true" Filterable="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Played Songs</MudText>
</ToolBarContent>
<Columns>
<Column T="SongPlayData" Sortable="false" Filterable="false">
<CellTemplate>
<MudButton Variant="Variant.Outlined" Size="Size.Small"
OnClick="@(() => OnShowDetailsClick(context.Item))">
@(context.Item.ShowDetails ? "Hide" : "Show") Song Play Details
</MudButton>
</CellTemplate>
</Column>
<Column T="SongPlayData" Field="IsFavorite" Sortable="false" Title="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>
<Column T="SongPlayData" Field="Title" Title="Song Title"/>
<Column T="SongPlayData" Field="Artist" Title="Artist"/>
<Column T="SongPlayData" Field="TotalPlayCount" Title="Total Play Count" />
</Columns>
<ChildRowContent>
@if (context.ShowDetails)
{
<MudTr>
<td colspan="5">
<MudCard Elevation="0">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.body1">Song Play Details</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pa-0">
<MudTable Items="@context.SongPlaySubDataList" Context="SongPlayDetail" Elevation="0" Filter="data => data.ClearState != ClearState.NotPlayed">
<HeaderContent>
<MudTh>Difficulty</MudTh>
<MudTh>Clear State</MudTh>
<MudTh>Play Count</MudTh>
<MudTh>Rating</MudTh>
<MudTh>Score</MudTh>
<MudTh>Max Chain</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Difficulty">@SongPlayDetail.Difficulty</MudTd>
<MudTd DataLabel="Clear State">
@(SongPlayDetail.Score != 1000000 ? SongPlayDetail.ClearState : ClearState.Perfect)
</MudTd>
<MudTd DataLabel="Play Count">@SongPlayDetail.PlayCount</MudTd>
<MudTd DataLabel="Rating">@CalculateRating(SongPlayDetail.Score)</MudTd>
<MudTd DataLabel="Score">@SongPlayDetail.Score</MudTd>
<MudTd DataLabel="Max Chain">@SongPlayDetail.MaxChain</MudTd>
</RowTemplate>
</MudTable>
</MudCardContent>
</MudCard>
</td>
</MudTr>
}
</ChildRowContent>
<PagerContent>
<MudDataGridPager T="SongPlayData"/>
</PagerContent>
</MudDataGrid>
</MudExpansionPanel>
</MudExpansionPanels>
}
</MudContainer>
@code {
[Parameter]
public long CardId { get; set; }
private PlayOptions.FeverTranceShow feverTranceShow;
private PlayOptions.FastSlowIndicator fastSlowIndicator;
private UserDetail? userDetail;
private List<SongPlayData> songPlayDataList = new();
private bool isSavingOptions;
protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); <MudSkeleton Width="1184px" Height="57px"/>
userDetail = await Client.GetFromJsonAsync<UserDetail>($"api/UserDetail/{CardId}") ?? new(); <MudSkeleton Width="1184px" Height="57px"/>
songPlayDataList = userDetail.SongPlayDataList ?? new List<SongPlayData>(); <MudSkeleton Width="1184px" Height="57px"/>
feverTranceShow = userDetail.PlayOption.FeverTrance;
fastSlowIndicator = userDetail.PlayOption.FastSlowIndicator;
} }
else
private void OnShowDetailsClick(SongPlayData data)
{ {
data.ShowDetails = !data.ShowDetails; if (userDetail is null)
}
private async Task SaveOptions()
{
isSavingOptions = true;
var postData = new PlayOption
{ {
CardId = CardId, <MudText Typo="Typo.h3">No Data</MudText>
FastSlowIndicator = fastSlowIndicator,
FeverTrance = feverTranceShow
};
var result = await Client.PostAsJsonAsync("api/UserDetail/SetPlayOption", postData);
isSavingOptions = false;
}
private static string CalculateRating(int score)
{
var grade = SharedConstants.GRADES.Where(g => g.Score <= score).Select(g => g.Grade).Last();
return grade;
}
private async Task OnFavoriteToggled(SongPlayData data)
{
var options = new DialogOptions
{
CloseOnEscapeKey = false,
DisableBackdropClick = true,
FullWidth = true
};
var parameters = new DialogParameters();
parameters.Add("Data", data);
parameters.Add("CardId", CardId);
var dialog = DialogService.Show<FavoriteDialog>("Favorite", parameters, options);
var result = await dialog.Result;
if (result.Cancelled)
{
return;
} }
else
if ((bool)result.Data)
{ {
Logger.LogInformation("Changed!"); <MudExpansionPanels>
data.IsFavorite = !data.IsFavorite; <MudExpansionPanel Text="Total Result">
<MudList>
<MudListSubheader>Player Name: @userDetail.PlayerName</MudListSubheader>
<MudListItem>Total Score: @userDetail.TotalScore</MudListItem>
<MudListItem>Average Score: @userDetail.AverageScore</MudListItem>
<MudListItem>Played Song Count: @userDetail.PlayedSongCount / @userDetail.TotalSongCount</MudListItem>
<MudListItem>Cleared Stage Count: @userDetail.ClearedStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>No Miss Stage Count: @userDetail.NoMissStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>Full Chain Stage Count: @userDetail.FullChainStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>Perfect Stage Count: @userDetail.PerfectStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S and Above Stage Count: @userDetail.SAboveStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S+ and Above Stage Count: @userDetail.SPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S++ and Above Stage Count: @userDetail.SPlusPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
</MudList>
</MudExpansionPanel>
<MudExpansionPanel Text="PlayOptions">
<MudSelect @bind-Value="@playOption.FastSlowIndicator"
Label="FAST/SLOW show setting">
@foreach (var item in Enum.GetValues<PlayOptions.FastSlowIndicator>())
{
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@playOption.FeverTrance"
Label="FEVER/TRANCE show setting">
@foreach (var item in Enum.GetValues<PlayOptions.FeverTranceShow>())
{
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem>
}
</MudSelect>
<MudAutocomplete T="long" Label="Avatar setting"
@bind-Value="@playOption.AvatarId"
CoerceText="true" SearchFunc="@SearchAvatar"
ToStringFunc="@AvatarIdToString"
Dense="true"
MaxItems="@avatarMaxItems">
<MoreItemsTemplate>
@*<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
OnClick="() => { avatarMaxItems += 10; AvatarAutoComplete.Clear();}">
Load more options
</MudButton>*@
<MudText Align="Align.Center">
Only first 50 items are displayed
</MudText>
</MoreItemsTemplate>
</MudAutocomplete>
<MudAutocomplete T="long" Label="Navigator setting"
@bind-Value="@playOption.NavigatorId"
CoerceText="true" SearchFunc="@SearchNavigator"
ToStringFunc="@NavigatorIdToString"
Dense="true"
MaxItems="@avatarMaxItems">
<MoreItemsTemplate>
@*<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
OnClick="() => { avatarMaxItems += 10; AvatarAutoComplete.Clear();}">
Load more options
</MudButton>*@
<MudText Align="Align.Center">
Only first 50 items are displayed
</MudText>
</MoreItemsTemplate>
</MudAutocomplete>
<MudAutocomplete T="long" Label="Title setting"
@bind-Value="@playOption.TitleId"
CoerceText="true" SearchFunc="@SearchTitle"
ToStringFunc="@TitleIdToString"
Dense="true"
MaxItems="@avatarMaxItems">
<MoreItemsTemplate>
@*<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
OnClick="() => { avatarMaxItems += 10; AvatarAutoComplete.Clear();}">
Load more options
</MudButton>*@
<MudText Align="Align.Center">
Only first 50 items are displayed
</MudText>
</MoreItemsTemplate>
</MudAutocomplete>
<MudButton Disabled="@isSavingOptions"
OnClick="SaveOptions"
Variant="Variant.Filled"
Color="Color.Info">
@if (isSavingOptions)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/>
<MudText Class="ms-2">Saving...</MudText>
}
else
{
<MudIcon Icon="@Icons.Filled.Save"></MudIcon>
<MudText>Save</MudText>
}
</MudButton>
</MudExpansionPanel>
<MudExpansionPanel Text="SongPlayData">
<MudDataGrid T="SongPlayData"
Items="@songPlayDataList"
Sortable="true"
Filterable="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Played Songs</MudText>
</ToolBarContent>
<Columns>
<Column T="SongPlayData" Sortable="false" Filterable="false">
<CellTemplate>
<MudButton Variant="Variant.Outlined" Size="Size.Small"
OnClick="@(() => OnShowDetailsClick(context.Item))">
@(context.Item.ShowDetails ? "Hide" : "Show") Song Play Details
</MudButton>
</CellTemplate>
</Column>
<Column T="SongPlayData" Field="IsFavorite" Sortable="false" Title="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>
<Column T="SongPlayData" Field="Title" Title="Song Title"/>
<Column T="SongPlayData" Field="Artist" Title="Artist"/>
<Column T="SongPlayData" Field="TotalPlayCount" Title="Total Play Count"/>
<Column T="SongPlayData" Field="LastPlayTime" Title="Last Play Time"/>
</Columns>
<ChildRowContent>
@if (context.ShowDetails)
{
<MudTr>
<td colspan="5">
<MudCard Elevation="0">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.body1">Song Play Details</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pa-0">
<MudTable Items="@context.SongPlaySubDataList"
Context="SongPlayDetail"
Elevation="0"
Filter="data => data.ClearState != ClearState.NotPlayed">
<HeaderContent>
<MudTh>Difficulty</MudTh>
<MudTh>Clear State</MudTh>
<MudTh>Play Count</MudTh>
<MudTh>Rating</MudTh>
<MudTh>Score</MudTh>
<MudTh>Max Chain</MudTh>
<MudTh>Last Play Time</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Difficulty">@SongPlayDetail.Difficulty</MudTd>
<MudTd DataLabel="Clear State">
@(SongPlayDetail.Score != 1000000 ? SongPlayDetail.ClearState : ClearState.Perfect)
</MudTd>
<MudTd DataLabel="Play Count">@SongPlayDetail.PlayCount</MudTd>
<MudTd DataLabel="Rating">@CalculateRating(SongPlayDetail.Score)</MudTd>
<MudTd DataLabel="Score">@SongPlayDetail.Score</MudTd>
<MudTd DataLabel="Max Chain">@SongPlayDetail.MaxChain</MudTd>
<MudTd DataLabel="Last Play Time">@SongPlayDetail.LastPlayTime</MudTd>
</RowTemplate>
</MudTable>
</MudCardContent>
</MudCard>
</td>
</MudTr>
}
</ChildRowContent>
<PagerContent>
<MudDataGridPager T="SongPlayData"/>
</PagerContent>
</MudDataGrid>
</MudExpansionPanel>
</MudExpansionPanels>
} }
} }
} </MudContainer>

View File

@ -0,0 +1,163 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using SharedProject.common;
using SharedProject.models;
namespace MudAdmin.Pages;
public partial class User
{
[Inject]
public HttpClient Client { get; set; } = null!;
[Inject]
public IDialogService DialogService { get; set; } = null!;
[Inject]
public ILogger<User> Logger { get; set; } = null!;
[Parameter]
public long CardId { get; set; }
private PlayOption playOption = new();
private UserDetail? userDetail;
private List<SongPlayData> songPlayDataList = new();
private Dictionary<long, Navigator> navigatorDictionary = new();
private Dictionary<long, Title> titleDictionary = new();
private Dictionary<long, Avatar> avatarDictionary = new();
private bool isSavingOptions;
private int avatarMaxItems = 50;
private bool pageLoading = true;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
userDetail = await Client.GetFromJsonAsync<UserDetail>($"api/UserDetail/{CardId}");
if (userDetail is null)
{
pageLoading = false;
return;
}
songPlayDataList = userDetail.SongPlayDataList ?? new List<SongPlayData>();
playOption = userDetail.PlayOption;
var navigators = await Client.GetFromJsonAsync<Navigators>("data/navigator.json");
if (navigators?.NavigatorList != null)
{
this.navigatorDictionary = navigators.NavigatorList.ToDictionary(navigator => (long)navigator.Id);
}
var avatars = await Client.GetFromJsonAsync<Avatar[]>("data/avatar.json");
if (avatars != null)
{
this.avatarDictionary = avatars.ToDictionary(avatar => (long)avatar.Id);
}
var titles = await Client.GetFromJsonAsync<Title[]>("data/title.json");
if (titles != null)
{
this.titleDictionary = titles.ToDictionary(title => (long)title.Id);
}
pageLoading = false;
}
private void OnShowDetailsClick(SongPlayData data)
{
data.ShowDetails = !data.ShowDetails;
}
private async Task SaveOptions()
{
isSavingOptions = true;
var postData = new PlayOption
{
CardId = CardId,
FastSlowIndicator = playOption.FastSlowIndicator,
FeverTrance = playOption.FeverTrance,
AvatarId = playOption.AvatarId,
NavigatorId = playOption.NavigatorId,
TitleId = playOption.TitleId
};
var result = await Client.PostAsJsonAsync("api/UserDetail/SetPlayOption", postData);
isSavingOptions = false;
}
private static string CalculateRating(int score)
{
var grade = SharedConstants.GRADES.Where(g => g.Score <= score).Select(g => g.Grade).Last();
return grade;
}
private async Task OnFavoriteToggled(SongPlayData data)
{
var options = new DialogOptions
{
CloseOnEscapeKey = false,
DisableBackdropClick = true,
FullWidth = true
};
var parameters = new DialogParameters();
parameters.Add("Data", data);
parameters.Add("CardId", CardId);
var dialog = DialogService.Show<FavoriteDialog>("Favorite", parameters, options);
var result = await dialog.Result;
if (result.Cancelled)
{
return;
}
if ((bool)result.Data)
{
Logger.LogInformation("Changed!");
data.IsFavorite = !data.IsFavorite;
}
}
private Task<IEnumerable<long>> SearchAvatar(string value)
{
var result = string.IsNullOrEmpty(value) ?
avatarDictionary.Keys :
avatarDictionary.Where(pair => pair.Value.ToString().Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(pair => pair.Key);
return Task.FromResult(result);
}
private Task<IEnumerable<long>> SearchTitle(string value)
{
var result = string.IsNullOrEmpty(value) ?
titleDictionary.Keys :
titleDictionary.Where(pair => pair.Value.ToString().Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(pair => pair.Key);
return Task.FromResult(result);
}
private Task<IEnumerable<long>> SearchNavigator(string value)
{
var result = string.IsNullOrEmpty(value) ?
navigatorDictionary.Keys :
navigatorDictionary.Where(pair => pair.Value.ToString().Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(pair => pair.Key);
return Task.FromResult(result);
}
private string AvatarIdToString(long id)
{
return avatarDictionary.ContainsKey(id) ? avatarDictionary[id].ToString() : $"No Data for {id}!";
}
private string NavigatorIdToString(long id)
{
return navigatorDictionary.ContainsKey(id) ? navigatorDictionary[id].ToString() : $"No Data for {id}!";
}
private string TitleIdToString(long id)
{
return titleDictionary.ContainsKey(id) ? titleDictionary[id].ToString() : $"No Data for {id}!";
}
}

View File

@ -30,7 +30,7 @@
} }
</MudGrid> </MudGrid>
} }
else if (!(users.Count == 0)) else if (users.Count != 0)
{ {
<MudGrid> <MudGrid>
@foreach (var user in users) @foreach (var user in users)

View File

@ -1,105 +0,0 @@
using SharedProject.models;
using GenFu;
using SharedProject.enums;
namespace MudAdmin.Utils;
public class MockDataRepo
{
private static readonly MockDataRepo INSTANCE = new MockDataRepo();
public List<User> Users { get; }
public List<UserDetail> UserDetails { get; private set; } = null!;
public List<SongPlayData> SongPlayDataList { get; private set; } = null!;
private MockDataRepo()
{
ConfigureGenFu();
Users = GenFu.GenFu.ListOf<User>(10);
GenerateUserDetails();
GenerateSongPlayData();
}
private void GenerateSongPlayData()
{
SongPlayDataList = GenFu.GenFu.ListOf<SongPlayData>();
foreach (var songPlayData in SongPlayDataList)
{
var subDataList = new List<SongPlayDetailData>();
var random = new Random();
foreach (var difficulty in Enum.GetValues<Difficulty>())
{
if (random.Next() <= int.MaxValue / 2)
{
continue;
}
var subData = GenFu.GenFu.New<SongPlayDetailData>();
subData.Difficulty = difficulty;
if (subData.ClearState == ClearState.Perfect)
{
subData.ClearState = ClearState.FullChain;
}
subDataList.Add(subData);
}
songPlayData.SongPlaySubDataList = subDataList.ToArray();
}
}
private void GenerateUserDetails()
{
UserDetails = new List<UserDetail>();
foreach (var user in Users)
{
var detail = GenFu.GenFu.New<UserDetail>();
detail.CardId = user.CardId;
detail.PlayerName = user.PlayerName;
detail.PlayOption = new PlayOption
{
CardId = user.CardId,
FeverTrance = PlayOptions.FeverTranceShow.Show,
FastSlowIndicator = PlayOptions.FastSlowIndicator.NotUsed
};
detail.AverageScore = 900000;
detail.TotalScore = 10000000;
detail.TotalSongCount = 123;
detail.TotalStageCount = 390;
UserDetails.Add(detail);
}
}
public static MockDataRepo GetMockDataRepo()
{
return INSTANCE;
}
private void ConfigureGenFu()
{
GenFu.GenFu.Configure<User>()
.Fill(user => user.CardId, () => new Random().NextInt64(7000000000000000, 8000000000000000));
GenFu.GenFu.Configure<UserDetail>()
.Fill(detail => detail.PlayedSongCount).WithinRange(100, 123)
.Fill(detail => detail.ClearedStageCount).WithinRange(300, 390)
.Fill(detail => detail.NoMissStageCount).WithinRange(200, 300)
.Fill(detail => detail.FullChainStageCount).WithinRange(100, 200)
.Fill(detail => detail.PerfectStageCount).WithinRange(0, 100)
.Fill(detail => detail.SAboveStageCount).WithinRange(200, 300)
.Fill(detail => detail.SPlusAboveStageCount).WithinRange(100, 200)
.Fill(detail => detail.SPlusPlusAboveStageCount).WithinRange(0, 100);
GenFu.GenFu.Configure<SongPlayDetailData>()
.Fill(data => data.Score).WithinRange(0, 1000001);
GenFu.GenFu.Configure<SongPlayData>()
.Fill(data => data.ShowDetails, false);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
namespace SharedProject.enums;
public enum NavigatorDefaultAvailability
{
NotAvailable = 0,
Available = 1,
AvailableWithVoice = 2,
}

View File

@ -0,0 +1,11 @@
namespace SharedProject.enums;
public enum NavigatorGenre
{
Default = 1,
Original = 2,
Game = 3,
Touhou = 4,
Vocaloid = 5,
Collab = 6,
}

View File

@ -0,0 +1,36 @@
namespace SharedProject.enums;
public enum TitleUnlockType
{
Invalid = 0,
Default = 1,
Clear = 2,
NoMiss = 3,
FullChain = 4,
SRankSimpleStages = 5,
SRankNormalStages = 6,
SRankHardStages = 7,
SRankExtraStages = 8,
SRankAllDifficulties = 9,
SPlusRankAllDifficulties = 10,
SPlusPlusRankAllDifficulties = 11,
Event = 12,
Prefecture = 13,
ChainMilestone = 14,
Adlibs = 15,
ConsecutiveNoMiss = 16,
ClearsUsingItems = 17,
Avatars = 18,
MultiplayerStarsTotal = 19,
SongSet20 = 20,
SongSet21 = 21,
SongSet22 = 22,
SongSet23 = 23,
SongSet24 = 24,
SongSet25 = 25,
SongSet26 = 26,
ProfileLevel = 27,
Perfect = 28,
OnlineMatching = 29,
Trophies = 30,
}

View File

@ -0,0 +1,15 @@
namespace SharedProject.models;
public class Avatar
{
public uint Id { get; set; }
public string? IdString { get; set; }
public string? FullName { get; set; }
public string? Name { get; set; }
public string? Variant { get; set; }
public string? AcquireMethod { get; set; }
public override string ToString() {
return $"{Id}: {FullName}, {AcquireMethod}";
}
}

View File

@ -0,0 +1,12 @@
namespace SharedProject.models;
public class NameEntry
{
public string? NameWithVariant { get; set; }
public string? NameWithoutVariant{ get; set; }
public string? Variant{ get; set; }
public string? IllustrationCredit{ get; set; }
public override string ToString() {
return $"{NameWithVariant}";
}
}

View File

@ -0,0 +1,25 @@
using SharedProject.enums;
namespace SharedProject.models;
public class Navigator
{
public uint Id { get; set; }
public string? IdString { get; set; }
public string? FileName { get; set; }
public NameEntry? NameEntry0 { get; set; }
public NameEntry? NameEntry1 { get; set; }
public NavigatorGenre Genre { get; set; }
public NavigatorDefaultAvailability DefaultAvailability { get; set; }
public string? ToolTipJp { get; set; }
public string? ToolTipEn { get; set; }
public override string ToString() {
return $"{Id}: {NameEntry1}, {ToolTipEn}";
}
}

View File

@ -0,0 +1,8 @@
namespace SharedProject.models;
public class Navigators
{
public int Count { get; set; }
public List<Navigator>? NavigatorList { get; set; }
}

View File

@ -13,4 +13,13 @@ public class PlayOption
[JsonPropertyName(nameof(FeverTrance))] [JsonPropertyName(nameof(FeverTrance))]
public PlayOptions.FeverTranceShow FeverTrance { get; set; } public PlayOptions.FeverTranceShow FeverTrance { get; set; }
[JsonPropertyName(nameof(AvatarId))]
public long AvatarId { get; set; }
[JsonPropertyName(nameof(NavigatorId))]
public long NavigatorId { get; set; }
[JsonPropertyName(nameof(TitleId))]
public long TitleId { get; set; }
} }

View File

@ -1,5 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using SharedProject.common; using SharedProject.common;
using SharedProject.enums;
namespace SharedProject.models; namespace SharedProject.models;
@ -25,4 +26,15 @@ public class SongPlayData
return SongPlaySubDataList.Sum(data => data.PlayCount); return SongPlaySubDataList.Sum(data => data.PlayCount);
} }
} }
[JsonIgnore]
public DateTime LastPlayTime
{
get
{
var songPlayDetailData = SongPlaySubDataList.Where(data => data.ClearState != ClearState.NotPlayed)
.MinBy(data => data.LastPlayTime);
return songPlayDetailData?.LastPlayTime ?? DateTime.MaxValue;
}
}
} }

View File

@ -13,4 +13,6 @@ public class SongPlayDetailData
public Difficulty Difficulty { get; set; } public Difficulty Difficulty { get; set; }
public ClearState ClearState { get; set; } public ClearState ClearState { get; set; }
public DateTime LastPlayTime { get; set; }
} }

View File

@ -0,0 +1,17 @@
using SharedProject.enums;
namespace SharedProject.models;
public class Title
{
public uint Id { get; set; }
public string? IdString { get; set; }
public string? NameJp { get; set; }
public string? NameEng { get; set; }
public string? UnlockRequirementJp { get; set; }
public string? UnlockRequirementEng { get; set; }
public TitleUnlockType Type { get; set; }
public override string ToString() {
return $"{Id}: {NameEng}, {UnlockRequirementEng}";
}
}