1
0
mirror of synced 2024-11-27 23:50:49 +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.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3); && detail.Pcol3 == Configs.CONFIG_PCOL3);
if (!existing.Any()) var secondConfig = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == data.CardId
&& detail.Pcol1 == Configs.SECOND_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3);
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;
firstDetail.Fcol2 = (int)data.TitleId;
var result = cardSqLiteConnection.Update(cardDetail); var secondDetail = secondConfig.First();
secondDetail.ScoreI1 = data.NavigatorId;
return result == 1; 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!")
@ -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

@ -91,6 +91,10 @@ public class CardDetail : Record, ICardIdModel
[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)
{ {
CardId = cardId; CardId = 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"/> <MudSkeleton Width="1184px" Height="57px"/>
<MudSkeleton Width="1184px" Height="57px"/> <MudSkeleton Width="1184px" Height="57px"/>
} }
else else
{ {
<MudExpansionPanels> if (userDetail is null)
<MudExpansionPanel Text="Total Result"> {
<MudList> <MudText Typo="Typo.h3">No Data</MudText>
<MudListSubheader>Player Name: @userDetail.PlayerName</MudListSubheader> }
<MudListItem>Total Score: @userDetail.TotalScore</MudListItem> else
<MudListItem>Average Score: @userDetail.AverageScore</MudListItem> {
<MudListItem>Played Song Count: @userDetail.PlayedSongCount / @userDetail.TotalSongCount</MudListItem> <MudExpansionPanels>
<MudListItem>Cleared Stage Count: @userDetail.ClearedStageCount / @userDetail.TotalStageCount</MudListItem> <MudExpansionPanel Text="Total Result">
<MudListItem>No Miss Stage Count: @userDetail.NoMissStageCount / @userDetail.TotalStageCount</MudListItem> <MudList>
<MudListItem>Full Chain Stage Count: @userDetail.FullChainStageCount / @userDetail.TotalStageCount</MudListItem> <MudListSubheader>Player Name: @userDetail.PlayerName</MudListSubheader>
<MudListItem>Perfect Stage Count: @userDetail.PerfectStageCount / @userDetail.TotalStageCount</MudListItem> <MudListItem>Total Score: @userDetail.TotalScore</MudListItem>
<MudListItem>S and Above Stage Count: @userDetail.SAboveStageCount / @userDetail.TotalStageCount</MudListItem> <MudListItem>Average Score: @userDetail.AverageScore</MudListItem>
<MudListItem>S+ and Above Stage Count: @userDetail.SPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem> <MudListItem>Played Song Count: @userDetail.PlayedSongCount / @userDetail.TotalSongCount</MudListItem>
<MudListItem>S++ and Above Stage Count: @userDetail.SPlusPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem> <MudListItem>Cleared Stage Count: @userDetail.ClearedStageCount / @userDetail.TotalStageCount</MudListItem>
</MudList> <MudListItem>No Miss Stage Count: @userDetail.NoMissStageCount / @userDetail.TotalStageCount</MudListItem>
</MudExpansionPanel> <MudListItem>Full Chain Stage Count: @userDetail.FullChainStageCount / @userDetail.TotalStageCount</MudListItem>
<MudExpansionPanel Text="PlayOptions"> <MudListItem>Perfect Stage Count: @userDetail.PerfectStageCount / @userDetail.TotalStageCount</MudListItem>
<MudSelect @bind-Value="@fastSlowIndicator" Label="FAST/SLOW show setting"> <MudListItem>S and Above Stage Count: @userDetail.SAboveStageCount / @userDetail.TotalStageCount</MudListItem>
@foreach (var item in Enum.GetValues<PlayOptions.FastSlowIndicator>()) <MudListItem>S+ and Above Stage Count: @userDetail.SPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
{ <MudListItem>S++ and Above Stage Count: @userDetail.SPlusPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem> </MudList>
} </MudExpansionPanel>
</MudSelect> <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="@feverTranceShow" Label="FEVER/TRANCE show setting"> <MudSelect @bind-Value="@playOption.FeverTrance"
@foreach (var item in Enum.GetValues<PlayOptions.FeverTranceShow>()) Label="FEVER/TRANCE show setting">
{ @foreach (var item in Enum.GetValues<PlayOptions.FeverTranceShow>())
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem> {
} <MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem>
</MudSelect> }
</MudSelect>
<MudButton Disabled="@isSavingOptions" OnClick="SaveOptions" Variant="Variant.Filled" Color="Color.Info"> <MudAutocomplete T="long" Label="Avatar setting"
@if (isSavingOptions) @bind-Value="@playOption.AvatarId"
{ CoerceText="true" SearchFunc="@SearchAvatar"
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/> ToStringFunc="@AvatarIdToString"
<MudText Class="ms-2">Saving...</MudText> Dense="true"
} MaxItems="@avatarMaxItems">
else <MoreItemsTemplate>
{ @*<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
<MudIcon Icon="@Icons.Filled.Save"></MudIcon> OnClick="() => { avatarMaxItems += 10; AvatarAutoComplete.Clear();}">
<MudText>Save</MudText> Load more options
} </MudButton>*@
</MudButton> <MudText Align="Align.Center">
</MudExpansionPanel> Only first 50 items are displayed
<MudExpansionPanel Text="SongPlayData"> </MudText>
<MudDataGrid T="SongPlayData" Items="@songPlayDataList" Sortable="true" Filterable="true"> </MoreItemsTemplate>
<ToolBarContent> </MudAutocomplete>
<MudText Typo="Typo.h6">Played Songs</MudText>
</ToolBarContent> <MudAutocomplete T="long" Label="Navigator setting"
<Columns> @bind-Value="@playOption.NavigatorId"
<Column T="SongPlayData" Sortable="false" Filterable="false"> CoerceText="true" SearchFunc="@SearchNavigator"
<CellTemplate> ToStringFunc="@NavigatorIdToString"
<MudButton Variant="Variant.Outlined" Size="Size.Small" Dense="true"
OnClick="@(() => OnShowDetailsClick(context.Item))"> MaxItems="@avatarMaxItems">
@(context.Item.ShowDetails ? "Hide" : "Show") Song Play Details <MoreItemsTemplate>
</MudButton> @*<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
</CellTemplate> OnClick="() => { avatarMaxItems += 10; AvatarAutoComplete.Clear();}">
</Column> Load more options
<Column T="SongPlayData" Field="IsFavorite" Sortable="false" Title="Favorite"> </MudButton>*@
<CellTemplate> <MudText Align="Align.Center">
<MudToggleIconButton Toggled="@context.Item.IsFavorite" Only first 50 items are displayed
ToggledChanged="@(()=>OnFavoriteToggled(context.Item))" </MudText>
Icon="@Icons.Material.Filled.FavoriteBorder" Color="@Color.Secondary" Title="Add to favorite" </MoreItemsTemplate>
ToggledIcon="@Icons.Material.Filled.Favorite" ToggledColor="@Color.Secondary" ToggledTitle="Remove from favorite"/> </MudAutocomplete>
</CellTemplate>
</Column> <MudAutocomplete T="long" Label="Title setting"
<Column T="SongPlayData" Field="Title" Title="Song Title"/> @bind-Value="@playOption.TitleId"
<Column T="SongPlayData" Field="Artist" Title="Artist"/> CoerceText="true" SearchFunc="@SearchTitle"
<Column T="SongPlayData" Field="TotalPlayCount" Title="Total Play Count" /> ToStringFunc="@TitleIdToString"
</Columns> Dense="true"
<ChildRowContent> MaxItems="@avatarMaxItems">
@if (context.ShowDetails) <MoreItemsTemplate>
{ @*<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
<MudTr> OnClick="() => { avatarMaxItems += 10; AvatarAutoComplete.Clear();}">
<td colspan="5"> Load more options
<MudCard Elevation="0"> </MudButton>*@
<MudCardHeader> <MudText Align="Align.Center">
<CardHeaderContent> Only first 50 items are displayed
<MudText Typo="Typo.body1">Song Play Details</MudText> </MudText>
</CardHeaderContent> </MoreItemsTemplate>
</MudCardHeader> </MudAutocomplete>
<MudCardContent Class="pa-0">
<MudTable Items="@context.SongPlaySubDataList" Context="SongPlayDetail" Elevation="0" Filter="data => data.ClearState != ClearState.NotPlayed"> <MudButton Disabled="@isSavingOptions"
<HeaderContent> OnClick="SaveOptions"
<MudTh>Difficulty</MudTh> Variant="Variant.Filled"
<MudTh>Clear State</MudTh> Color="Color.Info">
<MudTh>Play Count</MudTh> @if (isSavingOptions)
<MudTh>Rating</MudTh> {
<MudTh>Score</MudTh> <MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/>
<MudTh>Max Chain</MudTh> <MudText Class="ms-2">Saving...</MudText>
</HeaderContent> }
<RowTemplate> else
<MudTd DataLabel="Difficulty">@SongPlayDetail.Difficulty</MudTd> {
<MudTd DataLabel="Clear State"> <MudIcon Icon="@Icons.Filled.Save"></MudIcon>
@(SongPlayDetail.Score != 1000000 ? SongPlayDetail.ClearState : ClearState.Perfect) <MudText>Save</MudText>
</MudTd> }
<MudTd DataLabel="Play Count">@SongPlayDetail.PlayCount</MudTd> </MudButton>
<MudTd DataLabel="Rating">@CalculateRating(SongPlayDetail.Score)</MudTd> </MudExpansionPanel>
<MudTd DataLabel="Score">@SongPlayDetail.Score</MudTd> <MudExpansionPanel Text="SongPlayData">
<MudTd DataLabel="Max Chain">@SongPlayDetail.MaxChain</MudTd> <MudDataGrid T="SongPlayData"
</RowTemplate> Items="@songPlayDataList"
</MudTable> Sortable="true"
</MudCardContent> Filterable="true">
</MudCard> <ToolBarContent>
</td> <MudText Typo="Typo.h6">Played Songs</MudText>
</MudTr> </ToolBarContent>
} <Columns>
</ChildRowContent> <Column T="SongPlayData" Sortable="false" Filterable="false">
<PagerContent> <CellTemplate>
<MudDataGridPager T="SongPlayData"/> <MudButton Variant="Variant.Outlined" Size="Size.Small"
</PagerContent> OnClick="@(() => OnShowDetailsClick(context.Item))">
</MudDataGrid> @(context.Item.ShowDetails ? "Hide" : "Show") Song Play Details
</MudExpansionPanel> </MudButton>
</MudExpansionPanels> </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> </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();
userDetail = await Client.GetFromJsonAsync<UserDetail>($"api/UserDetail/{CardId}") ?? new();
songPlayDataList = userDetail.SongPlayDataList ?? new List<SongPlayData>();
feverTranceShow = userDetail.PlayOption.FeverTrance;
fastSlowIndicator = userDetail.PlayOption.FastSlowIndicator;
}
private void OnShowDetailsClick(SongPlayData data)
{
data.ShowDetails = !data.ShowDetails;
}
private async Task SaveOptions()
{
isSavingOptions = true;
var postData = new PlayOption
{
CardId = CardId,
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;
}
if ((bool)result.Data)
{
Logger.LogInformation("Changed!");
data.IsFavorite = !data.IsFavorite;
}
}
}

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}";
}
}