1
0
mirror of synced 2024-11-28 00:20:53 +01:00

Merge pull request #29 from asesidaa/LoginRefactor

Implemented better authentication system, completed more localization…
This commit is contained in:
shibe 2024-05-16 21:16:20 -04:00 committed by GitHub
commit e5d30c8ca2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 5678 additions and 1806 deletions

View File

@ -0,0 +1,491 @@
// <auto-generated />
using System;
using GameDatabase.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GameDatabase.Migrations
{
[DbContext(typeof(TaikoDbContext))]
[Migration("20240203182355_AddUnlockedUraSongIdListToUserDatum")]
partial class AddUnlockedUraSongIdListToUserDatum
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0-rc.2.23480.1");
modelBuilder.Entity("GameDatabase.Entities.AiScoreDatum", b =>
{
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("AiScoreData");
});
modelBuilder.Entity("GameDatabase.Entities.AiSectionScoreDatum", b =>
{
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<int>("SectionIndex")
.HasColumnType("INTEGER");
b.Property<int>("Crown")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<bool>("IsWin")
.HasColumnType("INTEGER");
b.Property<uint>("MissCount")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<uint>("Score")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty", "SectionIndex");
b.ToTable("AiSectionScoreData");
});
modelBuilder.Entity("GameDatabase.Entities.Card", b =>
{
b.Property<string>("AccessCode")
.HasColumnType("TEXT");
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.HasKey("AccessCode");
b.HasIndex("Baid");
b.ToTable("Card", (string)null);
});
modelBuilder.Entity("GameDatabase.Entities.Credential", b =>
{
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Salt")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Baid");
b.ToTable("Credential", (string)null);
});
modelBuilder.Entity("GameDatabase.Entities.DanScoreDatum", b =>
{
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("DanId")
.HasColumnType("INTEGER");
b.Property<int>("DanType")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1);
b.Property<uint>("ArrivalSongCount")
.HasColumnType("INTEGER");
b.Property<uint>("ClearState")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0u);
b.Property<uint>("ComboCountTotal")
.HasColumnType("INTEGER");
b.Property<uint>("SoulGaugeTotal")
.HasColumnType("INTEGER");
b.HasKey("Baid", "DanId", "DanType");
b.ToTable("DanScoreData");
});
modelBuilder.Entity("GameDatabase.Entities.DanStageScoreDatum", b =>
{
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("DanId")
.HasColumnType("INTEGER");
b.Property<int>("DanType")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1);
b.Property<uint>("SongNumber")
.HasColumnType("INTEGER");
b.Property<uint>("BadCount")
.HasColumnType("INTEGER");
b.Property<uint>("ComboCount")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<uint>("HighScore")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<uint>("PlayScore")
.HasColumnType("INTEGER");
b.Property<uint>("TotalHitCount")
.HasColumnType("INTEGER");
b.HasKey("Baid", "DanId", "DanType", "SongNumber");
b.ToTable("DanStageScoreData");
});
modelBuilder.Entity("GameDatabase.Entities.SongBestDatum", b =>
{
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<uint>("BestCrown")
.HasColumnType("INTEGER");
b.Property<uint>("BestRate")
.HasColumnType("INTEGER");
b.Property<uint>("BestScore")
.HasColumnType("INTEGER");
b.Property<uint>("BestScoreRank")
.HasColumnType("INTEGER");
b.HasKey("Baid", "SongId", "Difficulty");
b.ToTable("SongBestData");
});
modelBuilder.Entity("GameDatabase.Entities.SongPlayDatum", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("Baid")
.HasColumnType("INTEGER");
b.Property<uint>("ComboCount")
.HasColumnType("INTEGER");
b.Property<uint>("Crown")
.HasColumnType("INTEGER");
b.Property<uint>("Difficulty")
.HasColumnType("INTEGER");
b.Property<uint>("DrumrollCount")
.HasColumnType("INTEGER");
b.Property<uint>("GoodCount")
.HasColumnType("INTEGER");
b.Property<uint>("HitCount")
.HasColumnType("INTEGER");
b.Property<uint>("MissCount")
.HasColumnType("INTEGER");
b.Property<uint>("OkCount")
.HasColumnType("INTEGER");
b.Property<DateTime>("PlayTime")
.HasColumnType("datetime");
b.Property<uint>("Score")
.HasColumnType("INTEGER");
b.Property<uint>("ScoreRank")
.HasColumnType("INTEGER");
b.Property<uint>("ScoreRate")
.HasColumnType("INTEGER");
b.Property<bool>("Skipped")
.HasColumnType("INTEGER");
b.Property<uint>("SongId")
.HasColumnType("INTEGER");
b.Property<uint>("SongNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Baid");
b.ToTable("SongPlayData");
});
modelBuilder.Entity("GameDatabase.Entities.UserDatum", b =>
{
b.Property<ulong>("Baid")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<uint>("AchievementDisplayDifficulty")
.HasColumnType("INTEGER");
b.Property<int>("AiWinCount")
.HasColumnType("INTEGER");
b.Property<uint>("ColorBody")
.HasColumnType("INTEGER");
b.Property<uint>("ColorFace")
.HasColumnType("INTEGER");
b.Property<uint>("ColorLimb")
.HasColumnType("INTEGER");
b.Property<string>("CostumeData")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CostumeFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DifficultyPlayedArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DifficultySettingArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("DisplayAchievement")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayDan")
.HasColumnType("INTEGER");
b.Property<string>("FavoriteSongsArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("GenericInfoFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsAdmin")
.HasColumnType("INTEGER");
b.Property<bool>("IsSkipOn")
.HasColumnType("INTEGER");
b.Property<bool>("IsVoiceOn")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastPlayDatetime")
.HasColumnType("datetime");
b.Property<uint>("LastPlayMode")
.HasColumnType("INTEGER");
b.Property<string>("MyDonName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<uint>("MyDonNameLanguage")
.HasColumnType("INTEGER");
b.Property<int>("NotesPosition")
.HasColumnType("INTEGER");
b.Property<short>("OptionSetting")
.HasColumnType("INTEGER");
b.Property<uint>("SelectedToneId")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TitleFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<uint>("TitlePlateId")
.HasColumnType("INTEGER");
b.Property<string>("TokenCountDict")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToneFlgArray")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("UnlockedSongIdList")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Baid");
b.ToTable("UserData");
});
modelBuilder.Entity("GameDatabase.Entities.AiScoreDatum", b =>
{
b.HasOne("GameDatabase.Entities.UserDatum", "Ba")
.WithMany()
.HasForeignKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("GameDatabase.Entities.AiSectionScoreDatum", b =>
{
b.HasOne("GameDatabase.Entities.AiScoreDatum", "Parent")
.WithMany("AiSectionScoreData")
.HasForeignKey("Baid", "SongId", "Difficulty")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
});
modelBuilder.Entity("GameDatabase.Entities.Card", b =>
{
b.HasOne("GameDatabase.Entities.UserDatum", "Ba")
.WithMany()
.HasForeignKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("GameDatabase.Entities.Credential", b =>
{
b.HasOne("GameDatabase.Entities.UserDatum", "Ba")
.WithMany()
.HasForeignKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("GameDatabase.Entities.DanScoreDatum", b =>
{
b.HasOne("GameDatabase.Entities.UserDatum", "Ba")
.WithMany()
.HasForeignKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("GameDatabase.Entities.DanStageScoreDatum", b =>
{
b.HasOne("GameDatabase.Entities.DanScoreDatum", "Parent")
.WithMany("DanStageScoreData")
.HasForeignKey("Baid", "DanId", "DanType")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
});
modelBuilder.Entity("GameDatabase.Entities.SongBestDatum", b =>
{
b.HasOne("GameDatabase.Entities.UserDatum", "Ba")
.WithMany()
.HasForeignKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("GameDatabase.Entities.SongPlayDatum", b =>
{
b.HasOne("GameDatabase.Entities.UserDatum", "Ba")
.WithMany()
.HasForeignKey("Baid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ba");
});
modelBuilder.Entity("GameDatabase.Entities.AiScoreDatum", b =>
{
b.Navigation("AiSectionScoreData");
});
modelBuilder.Entity("GameDatabase.Entities.DanScoreDatum", b =>
{
b.Navigation("DanStageScoreData");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GameDatabase.Migrations
{
/// <inheritdoc />
public partial class AddUnlockedUraSongIdListToUserDatum : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -0,0 +1,10 @@
namespace SharedProject.Models.Requests;
public class ChangePasswordRequest
{
public string AccessCode { get; set; } = string.Empty;
public string OldPassword { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
}

View File

@ -0,0 +1,6 @@
namespace SharedProject.Models.Requests;
public class GenerateOtpRequest
{
public uint Baid { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace SharedProject.Models.Requests;
public class LoginRequest
{
public string AccessCode { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}

View File

@ -0,0 +1,10 @@
namespace SharedProject.Models.Requests;
public class RegisterRequest
{
public string AccessCode { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public bool RegisterWithLastPlayTime { get; set; }
public DateTime LastPlayDateTime { get; set; }
public string InviteCode { get; set; } = string.Empty;
}

View File

@ -0,0 +1,6 @@
namespace SharedProject.Models.Requests;
public class ResetPasswordRequest
{
public uint Baid { get; set; }
}

View File

@ -1,8 +0,0 @@
namespace SharedProject.Models.Requests;
public class SetPasswordRequest
{
public uint Baid { get; set; }
public string Password { get; set; } = default!;
public string Salt { get; set; } = default!;
}

View File

@ -0,0 +1,8 @@
namespace SharedProject.Models.Requests;
public class VerifyOtpRequest
{
public string Otp { get; set; } = "";
public uint Baid { get; set; }
}

View File

@ -1,8 +0,0 @@
namespace SharedProject.Models.Responses;
public class DashboardResponse
{
public List<User> Users { get; set; } = new();
public List<UserCredential> UserCredentials { get; set; } = new();
}

View File

@ -0,0 +1,6 @@
namespace SharedProject.Models.Responses;
public class SongHistoryResponse
{
public List<SongHistoryData> SongHistoryData { get; set; } = new();
}

View File

@ -0,0 +1,48 @@
using SharedProject.Enums;
namespace SharedProject.Models;
public class SongHistoryData
{
public uint SongId { get; set; }
public SongGenre Genre { get; set; }
public string MusicName { get; set; } = string.Empty;
public string MusicArtist { get; set; } = string.Empty;
public Difficulty Difficulty { get; set; }
public int Stars { get; set; }
public bool ShowDetails { get; set; }
public uint Score { get; set; }
public CrownType Crown { get; set; }
public ScoreRank ScoreRank { get; set; }
public DateTime PlayTime { get; set; }
public bool IsFavorite { get; set; }
public uint GoodCount { get; set; }
public uint OkCount { get; set; }
public uint MissCount { get; set; }
public uint ComboCount { get; set; }
public uint HitCount { get; set; }
public uint DrumrollCount { get; set; }
public uint SongNumber { get; set; }
//public List<AiSectionBestData> AiSectionBestData { get; set; } = new();
//public bool ShowAiData { get; set; }
}

View File

@ -0,0 +1,7 @@
{
"AuthSettings": {
"JwtKey": "SuperSecretKeyAndHeresItsPadding",
"JwtIssuer": "http://localhost:5000",
"JwtAudience": "http://localhost:5000"
}
}

View File

@ -0,0 +1,280 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using TaikoLocalServer.Settings;
using OtpNet;
using SharedProject.Models.Requests;
using TaikoLocalServer.Filters;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class AuthController(IAuthService authService, IUserDatumService userDatumService,
IOptions<AuthSettings> settings) : BaseController<AuthController>
{
private readonly AuthSettings authSettings = settings.Value;
private string GenerateJwtToken(uint baid, bool isAdmin)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.JwtKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new(ClaimTypes.Name, baid.ToString()),
new(ClaimTypes.Role, isAdmin ? "Admin" : "User")
};
var token = new JwtSecurityToken(
issuer: authSettings.JwtIssuer,
audience: authSettings.JwtAudience,
expires: DateTime.UtcNow.AddHours(24),
signingCredentials: credentials,
claims: claims
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private static string ComputeHash(string inputPassword, string salt)
{
var encDataByte = Encoding.UTF8.GetBytes(inputPassword + salt);
var encodedData = Convert.ToBase64String(encDataByte);
encDataByte = Encoding.UTF8.GetBytes(encodedData);
encodedData = Convert.ToBase64String(encDataByte);
encDataByte = Encoding.UTF8.GetBytes(encodedData);
encodedData = Convert.ToBase64String(encDataByte);
encDataByte = Encoding.UTF8.GetBytes(encodedData);
encodedData = Convert.ToBase64String(encDataByte);
return encodedData;
}
private static Totp MakeTotp(uint baid)
{
var secretKey = (baid * 765 + 2023).ToString();
var base32String = Base32Encoding.ToString(Encoding.UTF8.GetBytes(secretKey));
var base32Bytes = Base32Encoding.ToBytes(base32String);
return new Totp(base32Bytes, step: 999999999);
}
private static string CreateSalt()
{
//Generate a cryptographic random number.
var randomNumber = new byte[32];
var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
var salt = Convert.ToBase64String(randomNumber);
// Return a Base64 string representation of the random number.
return salt;
}
private static bool VerifyOtp(string otp, uint baid)
{
var totp = MakeTotp(baid);
return totp.VerifyTotp(otp, out _);
}
[HttpPost("Login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginRequest loginRequest)
{
var accessCode = loginRequest.AccessCode;
var password = loginRequest.Password;
var card = await authService.GetCardByAccessCode(accessCode);
if (card == null)
return Unauthorized(new { message = "Access Code Not Found" });
var credential = await authService.GetCredentialByBaid(card.Baid);
if (credential == null)
return Unauthorized(new { message = "Credential Not Found" });
if (credential.Password == "")
return Unauthorized(new { message = "User Not Registered" });
// Hash the password with the salt
var hashedPassword = ComputeHash(password, credential.Salt);
if (credential.Password != hashedPassword)
return Unauthorized(new { message = "Invalid Password" });
// Get User information
var user = await userDatumService.GetFirstUserDatumOrNull(card.Baid);
if (user == null)
return Unauthorized(new { message = "User Does Not Exist" });
var authToken = GenerateJwtToken(card.Baid, user.IsAdmin);
// Return the token with key authToken
return Ok(new { authToken });
}
[HttpPost("LoginWithToken")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public IActionResult LoginWithToken()
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
return Ok();
}
[HttpPost("Register")]
[AllowAnonymous]
public async Task<IActionResult> Register(RegisterRequest registerRequest)
{
var accessCode = registerRequest.AccessCode;
var password = registerRequest.Password;
var lastPlayDateTime = registerRequest.LastPlayDateTime;
var registerWithLastPlayTime = registerRequest.RegisterWithLastPlayTime;
var inviteCode = registerRequest.InviteCode;
var card = await authService.GetCardByAccessCode(accessCode);
if (card == null)
return Unauthorized(new { message = "Access Code Not Found" });
var credential = await authService.GetCredentialByBaid(card.Baid);
if (credential == null)
return Unauthorized(new { message = "Credential Not Found" });
if (credential.Password != "")
return Unauthorized(new { message = "User Already Registered" });
if (registerWithLastPlayTime)
{
var invited = false;
if (inviteCode != "")
{
invited = VerifyOtp(inviteCode, card.Baid);
}
if (!invited)
{
var user = await userDatumService.GetFirstUserDatumOrNull(card.Baid);
if (user == null)
return Unauthorized(new { message = "User Does Not Exist" });
var diffMinutes = (lastPlayDateTime - user.LastPlayDatetime).Duration().TotalMinutes;
if (diffMinutes > 5)
return Unauthorized(new { message = "Wrong Last Play Time" });
}
}
// Hash the password with the salt
var salt = CreateSalt();
var hashedPassword = ComputeHash(password, salt);
var result = await authService.UpdatePassword(card.Baid, hashedPassword, salt);
return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" });
}
[HttpPost("ChangePassword")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> ChangePassword(ChangePasswordRequest changePasswordRequest)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin)
{
var requestBaid = authService.GetCardByAccessCode(changePasswordRequest.AccessCode).Result?.Baid;
if (requestBaid != tokenInfo.Value.baid)
{
return Forbid();
}
}
}
var accessCode = changePasswordRequest.AccessCode;
var oldPassword = changePasswordRequest.OldPassword;
var newPassword = changePasswordRequest.NewPassword;
var card = await authService.GetCardByAccessCode(accessCode);
if (card == null)
return Unauthorized(new { message = "Access Code Not Found" });
var credential = await authService.GetCredentialByBaid(card.Baid);
if (credential == null)
return Unauthorized(new { message = "Credential Not Found" });
if (credential.Password == "")
return Unauthorized(new { message = "User Not Registered" });
// Hash the password with the salt
var hashedOldPassword = ComputeHash(oldPassword, credential.Salt);
if (credential.Password != hashedOldPassword)
return Unauthorized(new { message = "Wrong Old Password" });
// Hash the new password with the salt
var salt = CreateSalt();
var hashedNewPassword = ComputeHash(newPassword, salt);
var result = await authService.UpdatePassword(card.Baid, hashedNewPassword, salt);
return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" });
}
[HttpPost("ResetPassword")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> ResetPassword(ResetPasswordRequest resetPasswordRequest)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && resetPasswordRequest.Baid != tokenInfo.Value.baid)
{
return Forbid();
}
}
var baid = resetPasswordRequest.Baid;
var credential = await authService.GetCredentialByBaid(baid);
if (credential == null)
return Unauthorized(new { message = "Credential Not Found" });
var result = await authService.UpdatePassword(baid, "", "");
return result ? Ok() : Unauthorized( new { message = "Failed to Reset Password" });
}
[HttpPost("GenerateOtp")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public IActionResult GenerateOtp(GenerateOtpRequest generateOtpRequest)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var totp = MakeTotp(generateOtpRequest.Baid);
return Ok(new { otp = totp.ComputeTotp() });
}
}

View File

@ -1,42 +1,80 @@
using SharedProject.Models.Requests;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using SharedProject.Models.Requests;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class CardsController : BaseController<CardsController>
public class CardsController(IAuthService authService, IOptions<AuthSettings> settings) : BaseController<CardsController>
{
private readonly ICardService cardService;
public CardsController(ICardService cardService)
{
this.cardService = cardService;
}
private readonly AuthSettings authSettings = settings.Value;
[HttpDelete("{accessCode}")]
public async Task<IActionResult> DeleteUser(string accessCode)
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> DeleteAccessCode(string accessCode)
{
var result = await cardService.DeleteCard(accessCode);
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
var card = await authService.GetCardByAccessCode(accessCode);
if (card == null)
{
return Unauthorized();
}
if (card.Baid != tokenInfo.Value.baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var result = await authService.DeleteCard(accessCode);
return result ? NoContent() : NotFound();
}
[HttpPost]
public async Task<IActionResult> BindAccessCode(BindAccessCodeRequest request)
[HttpPost("BindAccessCode")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> BindAccessCode(BindAccessCodeRequest bindAccessCodeRequest)
{
var accessCode = request.AccessCode;
var baid = request.Baid;
var existingCard = await cardService.GetCardByAccessCode(accessCode);
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != bindAccessCodeRequest.Baid)
{
return Forbid();
}
}
var accessCode = bindAccessCodeRequest.AccessCode;
var baid = bindAccessCodeRequest.Baid;
var existingCard = await authService.GetCardByAccessCode(accessCode);
if (existingCard is not null)
{
return BadRequest("Access code already exists");
}
var newCard = new Card
{
Baid = baid,
AccessCode = accessCode
};
await cardService.AddCard(newCard);
await authService.AddCard(newCard);
return NoContent();
}
}

View File

@ -1,25 +0,0 @@
using SharedProject.Models.Requests;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class CredentialsController : BaseController<CredentialsController>
{
private readonly ICredentialService credentialService;
public CredentialsController(ICredentialService credentialService)
{
this.credentialService = credentialService;
}
[HttpPost]
public async Task<IActionResult> UpdatePassword(SetPasswordRequest request)
{
var baid = request.Baid;
var password = request.Password;
var salt = request.Salt;
var result = await credentialService.UpdatePassword(baid, password, salt);
return result ? NoContent() : NotFound();
}
}

View File

@ -1,23 +1,37 @@
using SharedProject.Models;
using Microsoft.Extensions.Options;
using SharedProject.Models;
using SharedProject.Models.Responses;
using Swan.Mapping;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class DanBestDataController : BaseController<DanBestDataController>
public class DanBestDataController(IDanScoreDatumService danScoreDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<DanBestDataController>
{
private readonly IDanScoreDatumService danScoreDatumService;
public DanBestDataController(IDanScoreDatumService danScoreDatumService)
{
this.danScoreDatumService = danScoreDatumService;
}
private readonly AuthSettings authSettings = settings.Value;
[HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> GetDanBestData(uint baid)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != baid)
{
return Forbid();
}
}
// FIXME: Handle gaiden in here and web ui
var danScores = await danScoreDatumService.GetDanScoreDataList(baid, DanType.Normal);
var danDataList = new List<DanBestData>();

View File

@ -1,30 +0,0 @@
using SharedProject.Models.Responses;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("/api/[controller]")]
public class DashboardController : BaseController<DashboardController>
{
private readonly ICardService cardService;
private readonly ICredentialService credentialService;
public DashboardController(ICardService cardService, ICredentialService credentialService)
{
this.cardService = cardService;
this.credentialService = credentialService;
}
[HttpGet]
public async Task<DashboardResponse> GetDashboard()
{
var users = await cardService.GetUsersFromCards();
var credentials = await credentialService.GetUserCredentialsFromCredentials();
return new DashboardResponse
{
Users = users,
UserCredentials = credentials
};
}
}

View File

@ -1,21 +1,35 @@
using SharedProject.Models.Requests;
using Microsoft.Extensions.Options;
using SharedProject.Models.Requests;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class FavoriteSongsController : BaseController<FavoriteSongsController>
public class FavoriteSongsController(IUserDatumService userDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<FavoriteSongsController>
{
private readonly IUserDatumService userDatumService;
public FavoriteSongsController(IUserDatumService userDatumService)
{
this.userDatumService = userDatumService;
}
private readonly AuthSettings authSettings = settings.Value;
[HttpPost]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> UpdateFavoriteSong(SetFavoriteRequest request)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != request.Baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(request.Baid);
if (user is null)
@ -28,8 +42,23 @@ public class FavoriteSongsController : BaseController<FavoriteSongsController>
}
[HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> GetFavoriteSongs(uint baid)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null)

View File

@ -1,32 +1,38 @@
using Riok.Mapperly.Abstractions;
using Microsoft.Extensions.Options;
using Riok.Mapperly.Abstractions;
using SharedProject.Models.Responses;
using SharedProject.Models;
using GameDatabase.Entities;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class PlayDataController(IUserDatumService userDatumService, ISongBestDatumService songBestDatumService,
ISongPlayDatumService songPlayDatumService, IAuthService authService, IOptions<AuthSettings> settings)
: BaseController<PlayDataController>
{
[ApiController]
[Route("api/[controller]")]
public class PlayDataController : BaseController<PlayDataController>
{
private readonly IUserDatumService userDatumService;
private readonly ISongBestDatumService songBestDatumService;
private readonly ISongPlayDatumService songPlayDatumService;
private readonly SongBestResponseMapper _songBestResponseMapper; // Inject SongBestResponseMapper
public PlayDataController(IUserDatumService userDatumService, ISongBestDatumService songBestDatumService,
ISongPlayDatumService songPlayDatumService, SongBestResponseMapper songBestResponseMapper)
{
this.userDatumService = userDatumService;
this.songBestDatumService = songBestDatumService;
this.songPlayDatumService = songPlayDatumService;
_songBestResponseMapper = songBestResponseMapper; // Assign the injected mapper
}
private readonly AuthSettings authSettings = settings.Value;
[HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<ActionResult<SongBestResponse>> GetSongBestRecords(uint baid)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null)
{
@ -64,12 +70,10 @@ namespace TaikoLocalServer.Controllers.Api
SongBestData = songBestRecords
});
}
}
}
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)]
public partial class SongBestResponseMapper
{
public static partial SongPlayDatumDto MapToDto(SongPlayDatum entity);
}
}
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)]
public partial class SongBestResponseMapper
{
public static partial SongPlayDatumDto MapToDto(SongPlayDatum entity);
}

View File

@ -0,0 +1,68 @@
using Microsoft.Extensions.Options;
using SharedProject.Models;
using SharedProject.Models.Responses;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class PlayHistoryController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService,
IAuthService authService, IOptions<AuthSettings> settings) : BaseController<PlayDataController>
{
private readonly AuthSettings authSettings = settings.Value;
[HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<ActionResult<SongHistoryResponse>> GetSongHistory(uint baid)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null)
{
return NotFound();
}
var playLogs = await songPlayDatumService.GetSongPlayDatumByBaid(baid);
var songHistory = playLogs.Select(play => new SongHistoryData
{
SongId = play.SongId,
Difficulty = play.Difficulty,
Score = play.Score,
ScoreRank = play.ScoreRank,
Crown = play.Crown,
GoodCount = play.GoodCount,
OkCount = play.OkCount,
MissCount = play.MissCount,
HitCount = play.HitCount,
DrumrollCount = play.DrumrollCount,
ComboCount = play.ComboCount,
PlayTime = play.PlayTime,
SongNumber = play.SongNumber
})
.ToList();
var favoriteSongs = await userDatumService.GetFavoriteSongIds(baid);
var favoriteSet = favoriteSongs.ToHashSet();
foreach (var song in songHistory.Where(song => favoriteSet.Contains(song.SongId)))
{
song.IsFavorite = true;
}
return Ok(new SongHistoryResponse{SongHistoryData = songHistory});
}
}

View File

@ -1,22 +1,36 @@
using SharedProject.Models;
using Microsoft.Extensions.Options;
using SharedProject.Models;
using SharedProject.Utils;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("/api/[controller]/{baid}")]
public class UserSettingsController : BaseController<UserSettingsController>
public class UserSettingsController(IUserDatumService userDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<UserSettingsController>
{
private readonly IUserDatumService userDatumService;
public UserSettingsController(IUserDatumService userDatumService)
{
this.userDatumService = userDatumService;
}
private readonly AuthSettings authSettings = settings.Value;
[HttpGet]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<ActionResult<UserSetting>> GetUserSetting(uint baid)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null)
@ -75,8 +89,23 @@ public class UserSettingsController : BaseController<UserSettingsController>
}
[HttpPost]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> SaveUserSetting(uint baid, UserSetting userSetting)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null)

View File

@ -1,19 +1,78 @@
namespace TaikoLocalServer.Controllers.Api;
using Microsoft.Extensions.Options;
using SharedProject.Models;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class UsersController : BaseController<UsersController>
public class UsersController(IUserDatumService userDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<UsersController>
{
private readonly IUserDatumService userDatumService;
private readonly AuthSettings authSettings = settings.Value;
public UsersController(IUserDatumService userDatumService)
[HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<User?> GetUser(uint baid)
{
this.userDatumService = userDatumService;
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return null;
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != baid)
{
return null;
}
}
var user = await authService.GetUserByBaid(baid);
return user;
}
[HttpGet]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IEnumerable<User>> GetUsers()
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Array.Empty<User>();
}
if (!tokenInfo.Value.isAdmin)
{
return Array.Empty<User>();
}
}
return await authService.GetUsersFromCards();
}
[HttpDelete("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> DeleteUser(uint baid)
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != baid)
{
return Forbid();
}
}
var result = await userDatumService.DeleteUser(baid);
return result ? NoContent() : NotFound();

View File

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Filters
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AuthorizeIfRequiredAttribute(IOptions<AuthSettings> settings) : Attribute, IAsyncAuthorizationFilter
{
private readonly bool loginRequired = settings.Value.LoginRequired;
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (!loginRequired)
{
return; // Skip authorization if login is not required
}
var authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var policyProvider = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationPolicyProvider>();
var policy = await policyProvider.GetPolicyAsync(AuthorizationPolicyNames.Default);
if (policy != null)
{
var authResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, policy);
if (!authResult.Succeeded)
{
context.Result = new UnauthorizedResult();
}
}
}
}
public static class AuthorizationPolicyNames
{
public const string Default = "Default";
}
}

View File

@ -2,7 +2,7 @@
namespace TaikoLocalServer.Models;
public class MusicInfoes
public class MusicInfos
{
[JsonPropertyName("items")]
public List<MusicInfoEntry> MusicInfoEntries { get; set; } = new();

View File

@ -1,9 +1,12 @@
using System.Reflection;
using System.Text;
using Serilog.Sinks.File.Header;
using TaikoLocalServer.Logging;
using GameDatabase.Context;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.IdentityModel.Tokens;
using TaikoLocalServer.Middlewares;
using TaikoLocalServer.Services.Extentions;
using TaikoLocalServer.Settings;
@ -11,6 +14,7 @@ using Throw;
using Serilog;
using SharedProject.Utils;
using TaikoLocalServer.Controllers.Api;
using TaikoLocalServer.Filters;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
@ -39,6 +43,8 @@ try
builder.Configuration.AddJsonFile($"{configurationsDirectory}/Database.json", optional: false, reloadOnChange: false);
builder.Configuration.AddJsonFile($"{configurationsDirectory}/ServerSettings.json", optional: false, reloadOnChange: false);
builder.Configuration.AddJsonFile($"{configurationsDirectory}/DataSettings.json", optional: true, reloadOnChange: false);
builder.Configuration.AddJsonFile($"{configurationsDirectory}/AuthSettings.json", optional: true, reloadOnChange: false);
builder.Configuration.AddJsonFile("wwwroot/appsettings.json", optional: true, reloadOnChange: true); // Add appsettings.json
builder.Host.UseSerilog((context, configuration) =>
{
@ -65,6 +71,34 @@ try
builder.Services.AddSingleton<IGameDataService, GameDataService>();
builder.Services.Configure<ServerSettings>(builder.Configuration.GetSection(nameof(ServerSettings)));
builder.Services.Configure<DataSettings>(builder.Configuration.GetSection(nameof(DataSettings)));
builder.Services.Configure<AuthSettings>(builder.Configuration.GetSection(nameof(AuthSettings)));
// Read LoginRequired setting from appsettings.json
var loginRequired = builder.Configuration.GetValue<bool>("LoginRequired");
builder.Services.Configure<AuthSettings>(options => { options.LoginRequired = loginRequired; });
// Add Authentication with JWT
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration.GetSection(nameof(AuthSettings))["JwtIssuer"],
ValidAudience = builder.Configuration.GetSection(nameof(AuthSettings))["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetSection(nameof(AuthSettings))["JwtKey"] ?? throw new InvalidOperationException()))
};
});
builder.Services.AddScoped<AuthorizeIfRequiredAttribute>(); // Register the custom attribute
builder.Services.AddControllers().AddProtoBufNet();
builder.Services.AddDbContext<TaikoDbContext>(option =>
{
@ -126,6 +160,9 @@ try
app.UseStaticFiles();
app.UseRouting();
// Enable Authentication and Authorization middleware
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpLogging();
app.Use(async (context, next) =>
@ -136,6 +173,7 @@ try
{
Log.Error("Unknown request from: {RemoteIpAddress} {Method} {Path} {StatusCode}",
context.Connection.RemoteIpAddress, context.Request.Method, context.Request.Path, context.Response.StatusCode);
Log.Error("Request headers: {Headers}", context.Request.Headers);
}
});
app.MapControllers();

View File

@ -0,0 +1,139 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using GameDatabase.Context;
using SharedProject.Models;
using Swan.Mapping;
namespace TaikoLocalServer.Services;
public class AuthService(TaikoDbContext context) : IAuthService
{
public async Task<Card?> GetCardByAccessCode(string accessCode)
{
return await context.Cards.FindAsync(accessCode);
}
public async Task<User?> GetUserByBaid(uint baid)
{
var userDatum = await context.UserData.FindAsync(baid);
if (userDatum == null) return null;
var cardEntries = await context.Cards.Where(card => card.Baid == baid).ToListAsync();
return new User
{
Baid = userDatum.Baid,
AccessCodes = cardEntries.Select(card => card.AccessCode).ToList(),
IsAdmin = userDatum.IsAdmin
};
}
public async Task<List<User>> GetUsersFromCards()
{
var cardEntries = await context.Cards.ToListAsync();
var userEntries = await context.UserData.ToListAsync();
var users = userEntries.Select(userEntry => new User
{
Baid = userEntry.Baid,
AccessCodes = cardEntries.Where(cardEntry => cardEntry.Baid == userEntry.Baid).Select(cardEntry => cardEntry.AccessCode).ToList(),
IsAdmin = userEntry.IsAdmin
}).ToList();
return users;
}
public async Task AddCard(Card card)
{
context.Add(card);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCard(string accessCode)
{
var card = await context.Cards.FindAsync(accessCode);
if (card == null) return false;
context.Cards.Remove(card);
await context.SaveChangesAsync();
return true;
}
public async Task<List<UserCredential>> GetUserCredentialsFromCredentials()
{
return await context.Credentials.Select(credential => credential.CopyPropertiesToNew<UserCredential>(null)).ToListAsync();
}
public async Task AddCredential(Credential credential)
{
context.Add(credential);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCredential(uint baid)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
context.Credentials.Remove(credential);
await context.SaveChangesAsync();
return true;
}
public async Task<bool> UpdatePassword(uint baid, string password, string salt)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
credential.Password = password;
credential.Salt = salt;
await context.SaveChangesAsync();
return true;
}
public async Task<Credential?> GetCredentialByBaid(uint baid)
{
return await context.Credentials.FindAsync(baid);
}
public (uint baid, bool isAdmin)? ExtractTokenInfo(HttpContext httpContext)
{
var authHeader = httpContext.Request.Headers.Authorization.FirstOrDefault();
if (authHeader == null || !authHeader.StartsWith("Bearer "))
{
Console.WriteLine("Invalid auth header");
return null;
}
var token = authHeader["Bearer ".Length..].Trim();
var handler = new JwtSecurityTokenHandler();
if (!handler.CanReadToken(token))
{
Console.WriteLine("Invalid token");
return null;
}
var jwtToken = handler.ReadJwtToken(token);
if (jwtToken.ValidTo < DateTime.UtcNow)
{
Console.WriteLine("Token expired");
return null;
}
var claimBaid = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
var claimRole = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value;
if (claimBaid == null || claimRole == null)
{
Console.WriteLine("Invalid token claims");
return null;
}
if (!uint.TryParse(claimBaid, out var baid))
{
Console.WriteLine("Invalid baid");
return null;
}
var isAdmin = claimRole == "Admin";
return (baid, isAdmin);
}
}

View File

@ -1,47 +0,0 @@
using GameDatabase.Context;
using SharedProject.Models;
namespace TaikoLocalServer.Services;
public class CardService : ICardService
{
private readonly TaikoDbContext context;
public CardService(TaikoDbContext context)
{
this.context = context;
}
public async Task<Card?> GetCardByAccessCode(string accessCode)
{
return await context.Cards.FindAsync(accessCode);
}
public async Task<List<User>> GetUsersFromCards()
{
var cardEntries = await context.Cards.ToListAsync();
var userEntries = await context.UserData.ToListAsync();
var users = userEntries.Select(userEntry => new User
{
Baid = (uint)userEntry.Baid,
AccessCodes = cardEntries.Where(cardEntry => cardEntry.Baid == userEntry.Baid).Select(cardEntry => cardEntry.AccessCode).ToList(),
IsAdmin = userEntry.IsAdmin
}).ToList();
return users;
}
public async Task AddCard(Card card)
{
context.Add(card);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCard(string accessCode)
{
var card = await context.Cards.FindAsync(accessCode);
if (card == null) return false;
context.Cards.Remove(card);
await context.SaveChangesAsync();
return true;
}
}

View File

@ -1,51 +0,0 @@
using GameDatabase.Context;
using SharedProject.Models;
using Swan.Mapping;
namespace TaikoLocalServer.Services;
public class CredentialService : ICredentialService
{
private readonly TaikoDbContext context;
public CredentialService(TaikoDbContext context)
{
this.context = context;
}
public async Task<List<UserCredential>> GetUserCredentialsFromCredentials()
{
return await context.Credentials.Select(credential => credential.CopyPropertiesToNew<UserCredential>(null)).ToListAsync();
}
public async Task AddCredential(Credential credential)
{
context.Add(credential);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCredential(uint baid)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
context.Credentials.Remove(credential);
await context.SaveChangesAsync();
return true;
}
public async Task<bool> UpdatePassword(uint baid, string password, string salt)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
credential.Password = password;
credential.Salt = salt;
await context.SaveChangesAsync();
return true;
}
}

View File

@ -4,8 +4,7 @@ public static class ServiceExtensions
{
public static IServiceCollection AddTaikoDbServices(this IServiceCollection services)
{
services.AddScoped<ICardService, CardService>();
services.AddScoped<ICredentialService, CredentialService>();
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IUserDatumService, UserDatumService>();
services.AddScoped<ISongPlayDatumService, SongPlayDatumService>();
services.AddScoped<ISongBestDatumService, SongBestDatumService>();

View File

@ -65,11 +65,6 @@ public class GameDataService : IGameDataService
return musicsWithUra;
}
public ImmutableDictionary<uint, MusicInfoEntry> GetMusicInfoes()
{
return musicInfoes;
}
public ImmutableDictionary<uint, MovieData> GetMovieDataDictionary()
{
return movieDataDictionary;
@ -161,7 +156,7 @@ public class GameDataService : IGameDataService
var shopFolderDataPath = Path.Combine(dataPath, settings.ShopFolderDataFileName);
var tokenDataPath = Path.Combine(dataPath, settings.TokenDataFileName);
var lockedSongsDataPath = Path.Combine(dataPath, settings.LockedSongsDataFileName);
var qrCodeDataPath = Path.Combine(dataPath, settings.QRCodeDataFileName);
var qrCodeDataPath = Path.Combine(dataPath, settings.QrCodeDataFileName);
var encryptedFiles = new List<string>
{
@ -213,7 +208,7 @@ public class GameDataService : IGameDataService
await using var neiroFile = File.OpenRead(neiroPath);
await using var qrCodeDataFile = File.OpenRead(qrCodeDataPath);
var infoesData = await JsonSerializer.DeserializeAsync<MusicInfoes>(musicInfoFile);
var infosData = await JsonSerializer.DeserializeAsync<MusicInfos>(musicInfoFile);
var danData = await JsonSerializer.DeserializeAsync<List<DanData>>(danDataFile);
var gaidenData = await JsonSerializer.DeserializeAsync<List<DanData>>(gaidenDataFile);
var introData = await JsonSerializer.DeserializeAsync<List<SongIntroductionData>>(songIntroDataFile);
@ -227,7 +222,7 @@ public class GameDataService : IGameDataService
var neiroData = await JsonSerializer.DeserializeAsync<Neiros>(neiroFile);
var qrCodeData = await JsonSerializer.DeserializeAsync<List<QRCodeData>>(qrCodeDataFile);
InitializeMusicInfoes(infoesData);
InitializeMusicInfos(infosData);
InitializeDanData(danData);
@ -305,11 +300,11 @@ public class GameDataService : IGameDataService
eventFolderDictionary = eventFolderData.ToImmutableDictionary(d => d.FolderId);
}
private void InitializeMusicInfoes(MusicInfoes? infoesData)
private void InitializeMusicInfos(MusicInfos? infosData)
{
infoesData.ThrowIfNull("Shouldn't happen!");
infosData.ThrowIfNull("Shouldn't happen!");
musicInfoes = infoesData.MusicInfoEntries.ToImmutableDictionary(info => info.MusicId);
musicInfoes = infosData.MusicInfoEntries.ToImmutableDictionary(info => info.MusicId);
musics = musicInfoes.Select(pair => pair.Key)
.ToList();

View File

@ -0,0 +1,28 @@
using SharedProject.Models;
namespace TaikoLocalServer.Services.Interfaces;
public interface IAuthService
{
public Task<User?> GetUserByBaid(uint baid);
public Task<Card?> GetCardByAccessCode(string accessCode);
public Task<List<User>> GetUsersFromCards();
public Task AddCard(Card card);
public Task<bool> DeleteCard(string accessCode);
public Task<List<UserCredential>> GetUserCredentialsFromCredentials();
public Task AddCredential(Credential credential);
public Task<bool> DeleteCredential(uint baid);
public Task<bool> UpdatePassword(uint baid, string password, string salt);
public Task<Credential?> GetCredentialByBaid(uint baid);
public (uint baid, bool isAdmin)? ExtractTokenInfo(HttpContext httpContext);
}

View File

@ -1,14 +0,0 @@
using SharedProject.Models;
namespace TaikoLocalServer.Services.Interfaces;
public interface ICardService
{
public Task<Card?> GetCardByAccessCode(string accessCode);
public Task<List<User>> GetUsersFromCards();
public Task AddCard(Card card);
public Task<bool> DeleteCard(string accessCode);
}

View File

@ -1,14 +0,0 @@
using SharedProject.Models;
namespace TaikoLocalServer.Services.Interfaces;
public interface ICredentialService
{
public Task<List<UserCredential>> GetUserCredentialsFromCredentials();
public Task AddCredential(Credential credential);
public Task<bool> DeleteCredential(uint baid);
public Task<bool> UpdatePassword(uint baid, string password, string salt);
}

View File

@ -11,8 +11,6 @@ public interface IGameDataService
public List<uint> GetMusicWithUraList();
public ImmutableDictionary<uint, MusicInfoEntry> GetMusicInfoes();
public ImmutableDictionary<uint, SongIntroductionData> GetSongIntroductionDictionary();
public ImmutableDictionary<uint, MovieData> GetMovieDataDictionary();
@ -37,4 +35,3 @@ public interface IGameDataService
public ImmutableDictionary<string, uint> GetQRCodeDataDictionary();
}

View File

@ -3,18 +3,8 @@ using Throw;
namespace TaikoLocalServer.Services;
public class UserDatumService : IUserDatumService
public class UserDatumService(TaikoDbContext context) : IUserDatumService
{
private readonly TaikoDbContext context;
private readonly ILogger<UserDatumService> logger;
public UserDatumService(TaikoDbContext context, ILogger<UserDatumService> logger)
{
this.context = context;
this.logger = logger;
}
public async Task<UserDatum?> GetFirstUserDatumOrNull(uint baid)
{
return await context.UserData

View File

@ -0,0 +1,12 @@
namespace TaikoLocalServer.Settings;
public class AuthSettings
{
public string JwtKey { get; set; } = string.Empty;
public string JwtIssuer { get; set; } = string.Empty;
public string JwtAudience { get; set; } = string.Empty;
public bool LoginRequired { get; set; }
}

View File

@ -18,5 +18,9 @@ public class DataSettings
public string LockedSongsDataFileName { get; set; } = "locked_songs_data.json";
public string QRCodeDataFileName { get; set; } = "qrcode_data.json";
public string QrCodeDataFileName { get; set; } = "qrcode_data.json";
public string LockedCostumeDataFileName { get; set; } = "locked_costume_data.json";
public string LockedTitleDataFileName { get; set; } = "locked_title_data.json";
}

View File

@ -15,26 +15,29 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DotNetZip" Version="1.16.0" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.0-rc.1.23421.29" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0-rc.2.23480.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0-rc.2.23480.1">
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="protobuf-net" Version="3.2.30" />
<PackageReference Include="protobuf-net.AspNetCore" Version="3.2.12" />
<PackageReference Include="Riok.Mapperly" Version="3.5.0-next.1" />
<PackageReference Include="Riok.Mapperly" Version="3.5.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00334" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0-dev-00137" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File.Header" Version="1.0.2" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Swan.Core" Version="7.0.0-beta.2" />
<PackageReference Include="Swan.Logging" Version="6.0.2-beta.96" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Throw" Version="1.4.0" />
<PackageReference Include="Yoh.Text.Json.NamingPolicies" Version="1.0.0" />
<PackageReference Include="Yoh.Text.Json.NamingPolicies" Version="1.1.2" />
</ItemGroup>
<ItemGroup>
@ -45,6 +48,9 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\ServerSettings.json" />
<None Include="Configurations\AuthSettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="Configurations\ServerSettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@ -136,6 +142,12 @@
<Content Update="wwwroot\data\datatable\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\locked_costume_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\locked_title_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\don_cos_reward.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@ -0,0 +1,17 @@
{
"Kigurumi": [
],
"Head": [
],
"Body": [
],
"Face": [
],
"Puchi": [
]
}

View File

@ -0,0 +1,8 @@
{
"TitleNo": [
],
"TitlePlateNo": [
]
}

View File

@ -8,7 +8,7 @@
<MudMenu Icon="@Icons.Material.Filled.Translate" Color="Color.Inherit" Size="Size.Small" Dense="true">
@foreach (var culture in SupportedCultures)
{
<MudMenuItem OnClick="() => RequestCultureChange(culture.Key)">@culture.Value</MudMenuItem>
<MudMenuItem OnClick="() => RequestCultureChange(culture.Key)" OnTouch="() => RequestCultureChange(culture.Key)">@culture.Value</MudMenuItem>
}
</MudMenu>

View File

@ -1,7 +1,9 @@
@inherits LayoutComponentBase
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage
@inject HttpClient Client
@inject AuthService AuthService
<MudThemeProvider IsDarkMode="@_isDarkMode" Theme="@TaikoWebUiTheme" />
<MudThemeProvider IsDarkMode="@isDarkMode" Theme="@taikoWebUiTheme" />
<MudDialogProvider />
<MudSnackbarProvider />
@ -14,7 +16,7 @@
<ChooseLanguage />
</MudStack>
</MudAppBar>
<MudDrawer Elevation="0" Style="border-right:1px solid #ededf0" @bind-Open="_drawerOpen">
<MudDrawer Elevation="0" Style="border-right:1px solid #ededf0" @bind-Open="drawerOpen">
<MudDrawerHeader>
<MudText Typo="Typo.h6">TaikoWebUI</MudText>
</MudDrawerHeader>
@ -32,41 +34,47 @@
</MudLayout>
@code {
bool _drawerOpen = true;
bool _isDarkMode = false;
bool drawerOpen = true;
bool isDarkMode = false;
protected override async Task OnInitializedAsync()
{
var hasDrawerOpenEntry = await localStorage.ContainKeyAsync("drawerOpen");
var hasDrawerOpenEntry = await LocalStorage.ContainKeyAsync("drawerOpen");
if (hasDrawerOpenEntry)
{
_drawerOpen = await localStorage.GetItemAsync<bool>("drawerOpen");
drawerOpen = await LocalStorage.GetItemAsync<bool>("drawerOpen");
}
var hasDarkModeEntry = await localStorage.ContainKeyAsync("isDarkMode");
var hasDarkModeEntry = await LocalStorage.ContainKeyAsync("isDarkMode");
if (hasDarkModeEntry)
{
_isDarkMode = await localStorage.GetItemAsync<bool>("isDarkMode");
isDarkMode = await LocalStorage.GetItemAsync<bool>("isDarkMode");
}
if (AuthService.LoginRequired)
{
// If not logged in, attempt to use JwtToken from local storage to log in
await AuthService.LoginWithAuthToken();
}
}
private async Task DrawerToggle()
{
_drawerOpen = !_drawerOpen;
await localStorage.SetItemAsync("drawerOpen", _drawerOpen);
drawerOpen = !drawerOpen;
await LocalStorage.SetItemAsync("drawerOpen", drawerOpen);
}
private async Task ToggleDarkMode()
{
_isDarkMode = !_isDarkMode;
await localStorage.SetItemAsync("isDarkMode", _isDarkMode);
isDarkMode = !isDarkMode;
await LocalStorage.SetItemAsync("isDarkMode", isDarkMode);
}
private string DarkModeIcon => _isDarkMode ? Icons.Material.Filled.BrightnessLow : Icons.Material.Filled.Brightness2;
private string DarkModeIcon => isDarkMode ? Icons.Material.Filled.BrightnessLow : Icons.Material.Filled.Brightness2;
MudTheme TaikoWebUiTheme = new MudTheme()
readonly MudTheme taikoWebUiTheme = new()
{
Palette = new PaletteLight()
{

View File

@ -1,42 +1,43 @@
@inject LoginService LoginService
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@inject IDialogService DialogService
@using TaikoWebUI.Pages.Dialogs;
<MudNavMenu Rounded="true" Class="pa-2" Margin="Margin.Dense" Color="Color.Primary">
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">@Localizer["dashboard"]</MudNavLink>
@if (LoginService.IsAdmin || !LoginService.LoginRequired)
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">@Localizer["Dashboard"]</MudNavLink>
@if (AuthService.IsAdmin || !AuthService.LoginRequired)
{
<MudNavLink Href="/Users" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.People">@Localizer["Users"]</MudNavLink>
}
@{
var currentUser = LoginService.GetLoggedInUser();
var baid = AuthService.GetLoggedInBaid();
if (LoginService.LoginRequired && !LoginService.OnlyAdmin && !LoginService.IsLoggedIn) {
if (AuthService.LoginRequired && !AuthService.OnlyAdmin && !AuthService.IsLoggedIn) {
<MudDivider />
<MudNavLink Href="/Login" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Login">@Localizer["Log In"]</MudNavLink>
<MudNavLink Href="/Register" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.AddCard">@Localizer["Register"]</MudNavLink>
}
if (LoginService.IsLoggedIn && currentUser != null)
if (AuthService.IsLoggedIn)
{
<MudDivider />
<MudNavLink Href="@($"Users/{currentUser.Baid}/Profile")" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Person">@Localizer["Profile"]</MudNavLink>
<MudNavGroup Title="Play Data" Expanded="true" Icon="@Icons.Material.Filled.EmojiEvents">
<MudNavLink Href="@($"Users/{currentUser.Baid}/Songs")" Match="NavLinkMatch.All">@Localizer["Key_01"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/HighScores")" Match="NavLinkMatch.All">@Localizer["High Scores"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/DaniDojo")" Match="NavLinkMatch.All">@Localizer["Key_03"]</MudNavLink>
<MudNavLink Href="@($"Users/{baid}/Profile")" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Person">@Localizer["Profile"]</MudNavLink>
<MudNavGroup Title=@Localizer["Play Data"] Expanded="true" Icon="@Icons.Material.Filled.EmojiEvents">
<MudNavLink Href="@($"Users/{baid}/Songs")" Match="NavLinkMatch.All">@Localizer["Song List"]</MudNavLink>
<MudNavLink Href="@($"Users/{baid}/HighScores")" Match="NavLinkMatch.All">@Localizer["High Scores"]</MudNavLink>
<MudNavLink Href="@($"Users/{baid}/PlayHistory")" Match="NavLinkMatch.All">@Localizer["Play History"]</MudNavLink>
<MudNavLink Href="@($"Users/{baid}/DaniDojo")" Match="NavLinkMatch.All">@Localizer["Dani Dojo"]</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Settings" Expanded="_settingsOpen" Icon="@Icons.Material.Filled.Settings">
<MudNavGroup Title=@Localizer["Settings"] Expanded="settingsOpen" Icon="@Icons.Material.Filled.Settings">
<MudNavLink OnClick="ShowQrCode">@Localizer["Show QR Code"]</MudNavLink>
<MudNavLink Href="/ChangePassword" Match="NavLinkMatch.All">@Localizer["Change Password"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/AccessCode")" Match="NavLinkMatch.All">@Localizer["Access Codes"]</MudNavLink>
<MudNavLink Href="@($"Users/{baid}/AccessCode")" Match="NavLinkMatch.All">@Localizer["Access Codes"]</MudNavLink>
</MudNavGroup>
}
if (LoginService.IsLoggedIn)
if (AuthService.IsLoggedIn)
{
<MudDivider />
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Logout" IconColor="Color.Error" OnClick="Logout">@Localizer["Log Out"]</MudNavLink>
@ -45,14 +46,14 @@
</MudNavMenu>
@code {
private bool _settingsOpen = false;
private bool settingsOpen = false;
protected override void OnInitialized()
{
LoginService.LoginStatusChanged += HandleLoginStatusChanged;
AuthService.LoginStatusChanged += HandleAuthStatusChanged;
}
private void HandleLoginStatusChanged(object? sender, EventArgs e)
private void HandleAuthStatusChanged(object? sender, EventArgs e)
{
StateHasChanged();
}
@ -61,22 +62,25 @@
{
if (firstRender)
{
LoginService.LoginStatusChanged += HandleLoginStatusChanged;
AuthService.LoginStatusChanged += HandleAuthStatusChanged;
}
}
private void ShowQrCode()
private async Task ShowQrCode()
{
var user = await AuthService.GetLoggedInUser();
if (user == null) return;
var parameters = new DialogParameters
{
["user"] = LoginService.GetLoggedInUser()
["user"] = user
};
var options = new DialogOptions() { DisableBackdropClick = true };
DialogService.Show<UserQrCodeDialog>("QR Code", parameters, options);
var options = new DialogOptions { DisableBackdropClick = true };
await DialogService.ShowAsync<UserQrCodeDialog>(Localizer["QR Code"], parameters, options);
// Prevent the settings menu from closing
_settingsOpen = true;
settingsOpen = true;
}
private async Task Logout()
@ -89,7 +93,7 @@
if (result == true)
{
LoginService.Logout();
await AuthService.Logout();
NavigationManager.NavigateTo("/");
}
}

View File

@ -2,35 +2,84 @@
<MudCard Outlined="true" Elevation="0">
<MudCardHeader>
<MudText Typo="Typo.h6">Play History</MudText>
<MudGrid Spacing="2">
<MudItem xs="12" md="4">
<MudText Typo="Typo.h6">@Localizer["Play History"]</MudText>
</MudItem>
<MudItem xs="12" md="8">
<MudText Typo="Typo.h6">@Localizer["Total Plays"]:@Items.Count</MudText>
</MudItem>
</MudGrid>
</MudCardHeader>
@if (Items != null && Items.Count > 0)
@if (Items.Count > 0)
{
<MudCardContent Class="p-0">
<MudTable Items="Items" Elevation="0" Striped="true">
<HeaderContent>
<MudTh>@Localizer["Play Time"]</MudTh>
<MudTh>@Localizer["Difficulty"]</MudTh>
<MudTh>@Localizer["Crown"]</MudTh>
<MudTh>@Localizer["Rank"]</MudTh>
<MudTh>@Localizer["Score"]</MudTh>
<MudTh>@Localizer["Good"]</MudTh>
<MudTh>@Localizer["Ok"]</MudTh>
<MudTh>@Localizer["Bad"]</MudTh>
<MudTh>@Localizer["Drumroll"]</MudTh>
<MudTh>@Localizer["Max Combo"]</MudTh>
<MudTh>
<MudTableSortLabel InitialDirection="SortDirection.Descending" T="SongHistoryData" SortBy="x => x.PlayTime">
@Localizer["Play Time"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.Difficulty">
@Localizer["Difficulty"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.Crown">
@Localizer["Crown"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.ScoreRank">
@Localizer["Rank"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.Score">
@Localizer["Score"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.GoodCount">
@Localizer["Good"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.OkCount">
@Localizer["OK"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.MissCount">
@Localizer["Bad"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.DrumrollCount">
@Localizer["Drumroll"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongHistoryData" SortBy="x => x.ComboCount">
@Localizer["MAX Combo"]
</MudTableSortLabel>
</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.PlayTime.ToString(Localizer["DateFormat"])</MudTd>
<MudTd>
<img src="@ScoreUtils.GetDifficultyIcon(context.Difficulty)" alt="@context.Difficulty" title="@context.Difficulty" style="@IconStyle" />
<img src="@ScoreUtils.GetDifficultyIcon(context.Difficulty)" alt="@context.Difficulty" title="@context.Difficulty" style="@IconStyle"/>
</MudTd>
<MudTd>
<img src="@($"/images/crown_{context.Crown}.png")" alt="@(ScoreUtils.GetCrownText(context.Crown))" title="@(ScoreUtils.GetCrownText(context.Crown))" style="@IconStyle" />
<img src="@($"/images/crown_{context.Crown}.png")" alt="@(ScoreUtils.GetCrownText(context.Crown))" title="@(ScoreUtils.GetCrownText(context.Crown))" style="@IconStyle"/>
</MudTd>
<MudTd>
<img src="@($"/images/rank_{context.ScoreRank}.png")" alt="@(ScoreUtils.GetRankText(context.ScoreRank))" title="@(ScoreUtils.GetRankText(context.ScoreRank))" style="@IconStyle" />
@if (context.ScoreRank is not ScoreRank.None)
{
<img src="@($"/images/rank_{context.ScoreRank}.png")" alt="@(ScoreUtils.GetRankText(context.ScoreRank))" title="@(ScoreUtils.GetRankText(context.ScoreRank))" style="@IconStyle"/>
}
</MudTd>
<MudTd>@context.Score</MudTd>
<MudTd>@context.GoodCount</MudTd>
@ -46,13 +95,13 @@
{
<MudCardContent>
<MudText Typo="Typo.body2" Class="pt-4 pb-8" Color="Color.Surface" Align="Align.Center">
@Localizer["No play history found."]
@Localizer["No Play History Found"]
</MudText>
</MudCardContent>
}
</MudCard>
@code {
[Parameter] public List<SongPlayDatumDto> Items { get; set; } = new List<SongPlayDatumDto>();
[Parameter] public List<SongHistoryData> Items { get; set; } = new();
private const string IconStyle = "width:25px; height:25px;";
}

View File

@ -1,13 +1,13 @@
@using Microsoft.AspNetCore.Components;
@using System.Text.Json
@using TaikoWebUI.Pages.Dialogs;
@inject TaikoWebUI.Utilities.StringUtil StringUtil;
@inject Utilities.StringUtil StringUtil;
@inject IDialogService DialogService;
@inject LoginService LoginService;
@inject AuthService AuthService;
@inject HttpClient Client
@inject NavigationManager NavigationManager
@if (user is not null)
@if (User is not null)
{
<MudCard Outlined="true">
<MudCardHeader>
@ -21,30 +21,30 @@
<MudSkeleton Width="35%" Height="32px" />
}
@if (LoginService.LoginRequired && user?.IsAdmin == true)
@if (AuthService.LoginRequired && User?.IsAdmin == true)
{
<MudChip Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.TwoTone.AdminPanelSettings">@Localizer["Admin"]</MudChip>
}
</div>
<MudText Typo="Typo.caption">User ID: @user?.Baid</MudText>
<MudText Typo="Typo.caption">@Localizer["User"] BAID: @User?.Baid</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense="true" AnchorOrigin="Origin.BottomLeft"
TransformOrigin="Origin.TopLeft" Size="Size.Small">
<MudMenuItem Icon="@Icons.Material.Filled.QrCode"
OnClick="@(_ => ShowQrCode(user))"
OnTouch="@(_ => ShowQrCode(user))"
OnClick="@(_ => ShowQrCode(User))"
OnTouch="@(_ => ShowQrCode(User))"
IconColor="@Color.Primary">
@Localizer["Show QR Code"]
</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.FeaturedPlayList"
Href="@($"Users/{user.Baid}/AccessCode")"
Href="@($"Users/{User.Baid}/AccessCode")"
IconColor="@Color.Primary">
@Localizer["Access Codes"]
</MudMenuItem>
<MudDivider />
@if (LoginService.OnlyAdmin || LoginService.LoginRequired)
@if (AuthService.OnlyAdmin || AuthService.LoginRequired)
{
<MudMenuItem Icon="@Icons.Material.Filled.Lock"
Href="@($"/ChangePassword")"
@ -53,21 +53,31 @@
</MudMenuItem>
<MudDivider />
}
@if (LoginService.LoginRequired && LoginService.IsAdmin)
@if (AuthService.LoginRequired && AuthService.IsAdmin)
{
<MudMenuItem Icon="@Icons.Material.Filled.Password"
OnClick="@(_ => GenerateInviteCode(User.Baid))"
OnTouch="@(_ => GenerateInviteCode(User.Baid))"
IconColor="@Color.Primary">
@Localizer["Generate Invite Code"]
</MudMenuItem>
<MudDivider />
}
@if (AuthService.LoginRequired && AuthService.IsAdmin)
{
<MudMenuItem Icon="@Icons.Material.Filled.LockReset"
OnClick="@(_ => ResetPassword(user))"
OnTouch="@(_ => ResetPassword(user))"
OnClick="@(_ => ResetPassword(User))"
OnTouch="@(_ => ResetPassword(User))"
IconColor="@Color.Primary">
@Localizer["Unregister"]
</MudMenuItem>
<MudDivider />
}
@if (LoginService.AllowUserDelete)
@if (AuthService.AllowUserDelete)
{
<MudMenuItem Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteUser(user))"
OnTouch="@(_ => DeleteUser(user))"
OnClick="@(_ => DeleteUser(User))"
OnTouch="@(_ => DeleteUser(User))"
IconColor="@Color.Error">
@Localizer["Delete User"]
</MudMenuItem>
@ -76,11 +86,11 @@
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.body2" Style="font-weight:bold">Access Code</MudText>
<MudText Typo="Typo.body2" Style="font-weight:bold">@Localizer["Access Code"]</MudText>
<MudText Style="font-family:monospace;overflow:hidden;overflow-x:scroll">
@if (user.AccessCodes.Count > 0)
@if (User.AccessCodes.Count > 0)
{
@foreach (var digitGroup in StringUtil.SplitIntoGroups(user.AccessCodes[0], 4))
@foreach (var digitGroup in StringUtil.SplitIntoGroups(User.AccessCodes[0], 4))
{
<span class="mr-2">@digitGroup</span>
}
@ -89,14 +99,19 @@
<span class="mr-2">@Localizer["N/A"]</span>
}
</MudText>
@if (user.AccessCodes.Count > 1)
@if (User.AccessCodes.Count > 1)
{
<MudText Typo="Typo.caption">... and @(user.AccessCodes.Count - 1) other access code(s)</MudText>
<MudText Typo="Typo.caption">... @Localizer["and"] @(User.AccessCodes.Count - 1) @Localizer["other access code(s)"]</MudText>
}
else
{
// Empty line to keep the layout consistent
<MudText Typo="Typo.caption"> </MudText>
}
</MudCardContent>
<MudCardActions>
<MudStack Row="true" Style="width:100%" Spacing="4" Justify="Justify.FlexEnd">
<MudButton Href="@($"Users/{user.Baid}/Profile")"
<MudButton Href="@($"Users/{User.Baid}/Profile")"
Size="Size.Small" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Edit"
Color="Color.Primary">
@Localizer["edit profile"]
@ -104,15 +119,16 @@
<MudMenu Size="Size.Small"
Dense="true"
Color="Color.Primary"
Label="@Localizer["view play data"]"
Label="@Localizer["View Play Data"]"
StartIcon="@Icons.Material.Filled.EmojiEvents"
EndIcon="@Icons.Material.Filled.KeyboardArrowDown"
FullWidth="true"
AnchorOrigin="Origin.BottomCenter"
TransformOrigin="Origin.TopCenter">
<MudMenuItem Href="@($"Users/{user.Baid}/HighScores")">@Localizer["High Scores"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{user.Baid}/Songs")">@Localizer["Song List"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{user.Baid}/DaniDojo")">@Localizer["dani dojo"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{User.Baid}/HighScores")">@Localizer["High Scores"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{User.Baid}/PlayHistory")">@Localizer["Play History"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{User.Baid}/Songs")">@Localizer["Song List"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{User.Baid}/DaniDojo")">@Localizer["Dani Dojo"]</MudMenuItem>
</MudMenu>
</MudStack>
</MudCardActions>
@ -120,14 +136,14 @@
}
@code {
[Parameter] public User? user { get; set; }
private DashboardResponse? response;
[Parameter] public User? User { get; set; }
private UserSetting? userSetting;
protected override async Task OnInitializedAsync()
{ if (user is not null)
{
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{user.Baid}");
if (User is not null)
{
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{User.Baid}");
}
}
@ -138,21 +154,21 @@
["user"] = user
};
var options = new DialogOptions() { DisableBackdropClick = true };
DialogService.Show<UserQrCodeDialog>("QR Code", parameters, options);
var options = new DialogOptions { DisableBackdropClick = true };
DialogService.Show<UserQrCodeDialog>(Localizer["QR Code"], parameters, options);
return Task.CompletedTask;
}
private async Task ResetPassword(User user)
{
var options = new DialogOptions() { DisableBackdropClick = true };
if (LoginService.LoginRequired && !LoginService.IsAdmin)
var options = new DialogOptions { DisableBackdropClick = true };
if (AuthService.LoginRequired && !AuthService.IsAdmin)
{
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
"Only admin can reset password.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
return;
}
var parameters = new DialogParameters
@ -160,23 +176,19 @@
["user"] = user
};
var dialog = DialogService.Show<ResetPasswordConfirmDialog>("Reset Password", parameters, options);
var result = await dialog.Result;
if (result.Canceled) return;
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
var dialog = await DialogService.ShowAsync<ResetPasswordConfirmDialog>(Localizer["Reset Password"], parameters, options);
await dialog.Result;
}
private async Task DeleteUser(User user)
{
var options = new DialogOptions() { DisableBackdropClick = true };
if (!LoginService.AllowUserDelete)
var options = new DialogOptions { DisableBackdropClick = true };
if (!AuthService.AllowUserDelete)
{
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
"User deletion is disabled by admin.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
return;
}
var parameters = new DialogParameters
@ -184,13 +196,56 @@
["user"] = user
};
var dialog = DialogService.Show<UserDeleteConfirmDialog>("Delete User", parameters, options);
var dialog = await DialogService.ShowAsync<UserDeleteConfirmDialog>(Localizer["Delete User"], parameters, options);
var result = await dialog.Result;
if (result.Canceled) return;
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
LoginService.Logout();
if (user.Baid == AuthService.GetLoggedInBaid())
{
await AuthService.Logout();
}
NavigationManager.NavigateTo("/Users");
}
private async Task GenerateInviteCode(uint baid)
{
var request = new GenerateOtpRequest
{
Baid = baid
};
var responseMessage = await Client.PostAsJsonAsync("api/Auth/GenerateOtp", request);
if (!responseMessage.IsSuccessStatusCode)
{
await DialogService.ShowMessageBox(
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"], null, null, new DialogOptions { DisableBackdropClick = true });
return;
}
var responseContent = await responseMessage.Content.ReadAsStringAsync();
var responseJson = JsonSerializer.Deserialize<Dictionary<string, string>>(responseContent);
if (responseJson == null)
{
await DialogService.ShowMessageBox(
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"], null, null, new DialogOptions { DisableBackdropClick = true });
return;
}
var otp = responseJson["otp"];
var parameters = new DialogParameters
{
["otp"] = otp
};
var options = new DialogOptions { DisableBackdropClick = true };
await DialogService.ShowAsync<OTPDialog>(Localizer["Invite Code"], parameters, options);
}
}

View File

@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@ -60,6 +59,186 @@ namespace TaikoWebUI.Localization {
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string _Invite_Code__Optional__ {
get {
return ResourceManager.GetString("\"Invite Code (Optional)\"", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Access_Code {
get {
return ResourceManager.GetString("Access Code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Access_Code_is_Required {
get {
return ResourceManager.GetString("Access Code is Required", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Access_Codes {
get {
return ResourceManager.GetString("Access Codes", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Achievement_Panel {
get {
return ResourceManager.GetString("Achievement Panel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Achievement_Panel_Difficulty {
get {
return ResourceManager.GetString("Achievement Panel Difficulty", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Add_Access_Code {
get {
return ResourceManager.GetString("Add Access Code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string AI_Battle_Data {
get {
return ResourceManager.GetString("AI Battle Data", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string and {
get {
return ResourceManager.GetString("and", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Bad {
get {
return ResourceManager.GetString("Bad", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Best_Crown {
get {
return ResourceManager.GetString("Best Crown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Best_Rank {
get {
return ResourceManager.GetString("Best Rank", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Chojin {
get {
return ResourceManager.GetString("Chojin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Confirm_New_Password {
get {
return ResourceManager.GetString("Confirm New Password", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Confirm_Password {
get {
return ResourceManager.GetString("Confirm Password", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Confirm_Password_is_Required {
get {
return ResourceManager.GetString("Confirm Password is Required", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Copy_to_Clipboard {
get {
return ResourceManager.GetString("Copy to Clipboard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Course_Songs {
get {
return ResourceManager.GetString("Course Songs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Dani_Dojo {
get {
return ResourceManager.GetString("Dani Dojo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Dashboard {
get {
return ResourceManager.GetString("Dashboard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to MM/dd/yyyy h:mm:ss tt.
/// </summary>
@ -68,5 +247,626 @@ namespace TaikoWebUI.Localization {
return ResourceManager.GetString("DateFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Delete {
get {
return ResourceManager.GetString("Delete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Dialog_OK {
get {
return ResourceManager.GetString("Dialog OK", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Difficulty {
get {
return ResourceManager.GetString("Difficulty", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Easy {
get {
return ResourceManager.GetString("Easy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Eighth_Dan {
get {
return ResourceManager.GetString("Eighth Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Error {
get {
return ResourceManager.GetString("Error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Fifth_Dan {
get {
return ResourceManager.GetString("Fifth Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Fifth_Kyuu {
get {
return ResourceManager.GetString("Fifth Kyuu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Filter_by_Genre {
get {
return ResourceManager.GetString("Filter by Genre", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string First_Dan {
get {
return ResourceManager.GetString("First Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string First_Kyuu {
get {
return ResourceManager.GetString("First Kyuu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Fourth_Dan {
get {
return ResourceManager.GetString("Fourth Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Fourth_Kyuu {
get {
return ResourceManager.GetString("Fourth Kyuu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Gaiden {
get {
return ResourceManager.GetString("Gaiden", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Generate_Invite_Code {
get {
return ResourceManager.GetString("Generate Invite Code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Gold_Donderful_Combo {
get {
return ResourceManager.GetString("Gold Donderful Combo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Gold_Full_Combo {
get {
return ResourceManager.GetString("Gold Full Combo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Hard {
get {
return ResourceManager.GetString("Hard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string High_Scores {
get {
return ResourceManager.GetString("High Scores", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Invite_Code {
get {
return ResourceManager.GetString("Invite Code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Kuroto {
get {
return ResourceManager.GetString("Kuroto", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Last_Play_Date {
get {
return ResourceManager.GetString("Last Play Date", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Last_Play_Time_5_Min_Around_Credit_End_ {
get {
return ResourceManager.GetString("Last Play Time(5 Min Around Credit End)", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Log_In {
get {
return ResourceManager.GetString("Log In", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Log_In_First {
get {
return ResourceManager.GetString("Log In First", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Log_Out {
get {
return ResourceManager.GetString("Log Out", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Meijin {
get {
return ResourceManager.GetString("Meijin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string New_Access_Code {
get {
return ResourceManager.GetString("New Access Code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string New_Password {
get {
return ResourceManager.GetString("New Password", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Ninth_Dan {
get {
return ResourceManager.GetString("Ninth Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string No_Play_History_Found {
get {
return ResourceManager.GetString("No Play History Found", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Normal {
get {
return ResourceManager.GetString("Normal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Not_Cleared {
get {
return ResourceManager.GetString("Not Cleared", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Old_Password {
get {
return ResourceManager.GetString("Old Password", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Oni {
get {
return ResourceManager.GetString("Oni", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string other_access_code_s_ {
get {
return ResourceManager.GetString("other access code(s)", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Password {
get {
return ResourceManager.GetString("Password", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Password_is_Required {
get {
return ResourceManager.GetString("Password is Required", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Play_Data {
get {
return ResourceManager.GetString("Play Data", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Play_History {
get {
return ResourceManager.GetString("Play History", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Play_Time {
get {
return ResourceManager.GetString("Play Time", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string QR_Code {
get {
return ResourceManager.GetString("QR Code", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Rank {
get {
return ResourceManager.GetString("Rank", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Red_Donderful_Combo {
get {
return ResourceManager.GetString("Red Donderful Combo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Red_Full_Combo {
get {
return ResourceManager.GetString("Red Full Combo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Register {
get {
return ResourceManager.GetString("Register", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Rows_Per_Page_ {
get {
return ResourceManager.GetString("Rows Per Page:", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Search_by_Title__Artist_or_Date {
get {
return ResourceManager.GetString("Search by Title, Artist or Date", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Search_by_Title_or_Artist {
get {
return ResourceManager.GetString("Search by Title or Artist", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Second_Dan {
get {
return ResourceManager.GetString("Second Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Second_Kyuu {
get {
return ResourceManager.GetString("Second Kyuu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Settings {
get {
return ResourceManager.GetString("Settings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Seventh_Dan {
get {
return ResourceManager.GetString("Seventh Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Sixth_Dan {
get {
return ResourceManager.GetString("Sixth Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Song_List {
get {
return ResourceManager.GetString("Song List", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Song_Name {
get {
return ResourceManager.GetString("Song Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Song_Number {
get {
return ResourceManager.GetString("Song Number", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Song_Title___Artist {
get {
return ResourceManager.GetString("Song Title / Artist", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Success {
get {
return ResourceManager.GetString("Success", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Tatsujin {
get {
return ResourceManager.GetString("Tatsujin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Tenth_Dan {
get {
return ResourceManager.GetString("Tenth Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Third_Dan {
get {
return ResourceManager.GetString("Third Dan", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Third_Kyuu {
get {
return ResourceManager.GetString("Third Kyuu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Unknown_Error {
get {
return ResourceManager.GetString("Unknown Error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Unregister {
get {
return ResourceManager.GetString("Unregister", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Ura_Oni {
get {
return ResourceManager.GetString("Ura Oni", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string User {
get {
return ResourceManager.GetString("User", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string Users {
get {
return ResourceManager.GetString("Users", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
internal static string View_Play_Data {
get {
return ResourceManager.GetString("View Play Data", resourceCulture);
}
}
}
}

View File

@ -117,31 +117,28 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="dashboard" xml:space="preserve">
<data name="Dashboard" xml:space="preserve">
<value>Dashboard</value>
</data>
<data name="users" xml:space="preserve">
<data name="Users" xml:space="preserve">
<value>Users</value>
</data>
<data name="edit profile" xml:space="preserve">
<value>Edit Profile</value>
</data>
<data name="user" xml:space="preserve">
<data name="User" xml:space="preserve">
<value>User</value>
</data>
<data name="view play data" xml:space="preserve">
<data name="View Play Data" xml:space="preserve">
<value>View Play Data</value>
</data>
<data name="high scores" xml:space="preserve">
<value>Songs</value>
</data>
<data name="dani dojo" xml:space="preserve">
<value>Dani Dojo</value>
<data name="High Scores" xml:space="preserve">
<value>High Scores</value>
</data>
<data name="Show QR Code" xml:space="preserve">
<value>Show QR Code</value>
</data>
<data name="Manage Access Codes" xml:space="preserve">
<data name="Access Codes" xml:space="preserve">
<value>Manage Access Codes</value>
</data>
<data name="Change Password" xml:space="preserve">
@ -156,7 +153,7 @@
<data name="Welcome to TaikoWebUI!" xml:space="preserve">
<value>Welcome to TaikoWebUI!</value>
</data>
<data name="Song" xml:space="preserve">
<data name="Song Name" xml:space="preserve">
<value>Song</value>
</data>
<data name="Level" xml:space="preserve">
@ -207,7 +204,7 @@
<data name="Total Donderful Combos" xml:space="preserve">
<value>Total Donderful Combos</value>
</data>
<data name="Key_01" xml:space="preserve">
<data name="Song List" xml:space="preserve">
<value>Songs</value>
</data>
<data name="Hide" xml:space="preserve">
@ -229,9 +226,9 @@
<value>Crown</value>
</data>
<data name="No data." xml:space="preserve">
<value>No data.</value>
<value>No data</value>
</data>
<data name="Key_02" xml:space="preserve">
<data name="Log In First" xml:space="preserve">
<value>"Please log in by clicking on "Users" tab first.</value>
</data>
<data name="Total Hits" xml:space="preserve">
@ -240,20 +237,20 @@
<data name="Soul Gauge" xml:space="preserve">
<value>Soul Gauge</value>
</data>
<data name="Songs" xml:space="preserve">
<data name="Course Songs" xml:space="preserve">
<value>Songs</value>
</data>
<data name="Conditions" xml:space="preserve">
<value>Conditions</value>
</data>
<data name="Red" xml:space="preserve">
<value>Red</value>
<value>Red Clear</value>
</data>
<data name="Gold" xml:space="preserve">
<value>Gold</value>
</data>
<data name="Failed" xml:space="preserve">
<value>Failed</value>
<data name="Not Cleared" xml:space="preserve">
<value>Not Cleared</value>
</data>
<data name="Pass" xml:space="preserve">
<value>Pass</value>
@ -267,7 +264,7 @@
<data name="Stage" xml:space="preserve">
<value>Stage</value>
</data>
<data name="Key_03" xml:space="preserve">
<data name="Dani Dojo" xml:space="preserve">
<value>Dani Dojo</value>
</data>
<data name="Profile" xml:space="preserve">
@ -384,4 +381,217 @@
<data name="DateFormat" xml:space="preserve">
<value>MM/dd/yyyy h:mm:ss tt</value>
</data>
<data name="Generate Invite Code" xml:space="preserve">
<value>Generate Invite Code</value>
</data>
<data name="Register" xml:space="preserve">
<value>Register</value>
</data>
<data name="Log In" xml:space="preserve">
<value>Log In</value>
</data>
<data name="Log Out" xml:space="preserve">
<value>Log Out</value>
</data>
<data name="Play Data" xml:space="preserve">
<value>Play Data</value>
</data>
<data name="Add Access Code" xml:space="preserve">
<value>Add Access Code</value>
</data>
<data name="New Access Code" xml:space="preserve">
<value>New Access Code</value>
</data>
<data name="Delete" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Access Code" xml:space="preserve">
<value>Access Code</value>
</data>
<data name="Old Password" xml:space="preserve">
<value>Old Password</value>
</data>
<data name="New Password" xml:space="preserve">
<value>New Password</value>
</data>
<data name="Confirm New Password" xml:space="preserve">
<value>Confirm New Password</value>
</data>
<data name="Dialog OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="QR Code" xml:space="preserve">
<value>QR Code</value>
</data>
<data name="Chojin" xml:space="preserve">
<value>Chojin</value>
</data>
<data name="Eighth Dan" xml:space="preserve">
<value>Eighth Dan</value>
</data>
<data name="Fifth Dan" xml:space="preserve">
<value>5th Dan</value>
</data>
<data name="Fifth Kyuu" xml:space="preserve">
<value>5th Kyuu</value>
</data>
<data name="First Dan" xml:space="preserve">
<value>1st Dan</value>
</data>
<data name="First Kyuu" xml:space="preserve">
<value>1st Kyuu</value>
</data>
<data name="Fourth Dan" xml:space="preserve">
<value>4th Dan</value>
</data>
<data name="Fourth Kyuu" xml:space="preserve">
<value>4th Kyuu</value>
</data>
<data name="Gaiden" xml:space="preserve">
<value>Gaiden</value>
</data>
<data name="Kuroto" xml:space="preserve">
<value>Kuroto</value>
</data>
<data name="Meijin" xml:space="preserve">
<value>Meijin</value>
</data>
<data name="Ninth Dan" xml:space="preserve">
<value>9th Dan</value>
</data>
<data name="Second Dan" xml:space="preserve">
<value>2nd Dan</value>
</data>
<data name="Second Kyuu" xml:space="preserve">
<value>2nd Kyuu</value>
</data>
<data name="Seventh Dan" xml:space="preserve">
<value>7th Dan</value>
</data>
<data name="Sixth Dan" xml:space="preserve">
<value>6th Dan</value>
</data>
<data name="Tatsujin" xml:space="preserve">
<value>Tatsujin</value>
</data>
<data name="Tenth Dan" xml:space="preserve">
<value>10th Dan</value>
</data>
<data name="Third Dan" xml:space="preserve">
<value>3rd Dan</value>
</data>
<data name="Third Kyuu" xml:space="preserve">
<value>3rd Kyuu</value>
</data>
<data name="Gold Full Combo" xml:space="preserve">
<value>Gold Full Combo</value>
</data>
<data name="Red Donderful Combo" xml:space="preserve">
<value>Red Donderful Combo</value>
</data>
<data name="Red Full Combo" xml:space="preserve">
<value>Red Full Combo</value>
</data>
<data name="Gold Donderful Combo" xml:space="preserve">
<value>Gold Donderful Combo</value>
</data>
<data name="Song Title / Artist" xml:space="preserve">
<value>Song Title / Artist</value>
</data>
<data name="Search by Title or Artist" xml:space="preserve">
<value>Search by Title or Artist</value>
</data>
<data name="Filter by Genre" xml:space="preserve">
<value>Filter by Genre</value>
</data>
<data name="Play History" xml:space="preserve">
<value>Play History</value>
</data>
<data name="No Play History Found" xml:space="preserve">
<value>No play history found</value>
</data>
<data name="Password" xml:space="preserve">
<value>Password</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Play Time" xml:space="preserve">
<value>Play Time</value>
</data>
<data name="Rank" xml:space="preserve">
<value>Rank</value>
</data>
<data name="Difficulty" xml:space="preserve">
<value>Difficulty</value>
</data>
<data name="Song Number" xml:space="preserve">
<value>Song Number</value>
</data>
<data name="Search by Title, Artist or Date" xml:space="preserve">
<value>Search by Title, Artist or Date</value>
</data>
<data name="Unregister" xml:space="preserve">
<value>Unregister</value>
</data>
<data name="and" xml:space="preserve">
<value>and</value>
</data>
<data name="other access code(s)" xml:space="preserve">
<value>other Access Code(s)</value>
</data>
<data name="Copy to Clipboard" xml:space="preserve">
<value>Copy to Clipboard</value>
</data>
<data name="Invite Code" xml:space="preserve">
<value>Invite Code</value>
</data>
<data name="Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="Access Code is Required" xml:space="preserve">
<value>Access Code is required</value>
</data>
<data name="&quot;Invite Code (Optional)&quot;" xml:space="preserve">
<value>"Invite Code (Optional)"</value>
</data>
<data name="Last Play Date" xml:space="preserve">
<value>Last Play Date</value>
</data>
<data name="Last Play Time(5 Min Around Credit End)" xml:space="preserve">
<value>Last Play Time(5 Min Around Credit End)</value>
</data>
<data name="Password is Required" xml:space="preserve">
<value>Password is required</value>
</data>
<data name="Confirm Password" xml:space="preserve">
<value>Confirm Password</value>
</data>
<data name="Confirm Password is Required" xml:space="preserve">
<value>Confirm password is required</value>
</data>
<data name="Unknown Error" xml:space="preserve">
<value>Unknown Error</value>
</data>
<data name="Success" xml:space="preserve">
<value>Success</value>
</data>
<data name="Ura Oni" xml:space="preserve">
<value>Ura</value>
</data>
<data name="Oni" xml:space="preserve">
<value>Extreme</value>
</data>
<data name="Hard" xml:space="preserve">
<value>Hard</value>
</data>
<data name="Normal" xml:space="preserve">
<value>Normal</value>
</data>
<data name="Easy" xml:space="preserve">
<value>Easy</value>
</data>
<data name="Rows Per Page:" xml:space="preserve">
<value>Rows Per Page:</value>
</data>
</root>

View File

@ -117,31 +117,28 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="dashboard" xml:space="preserve">
<data name="Dashboard" xml:space="preserve">
<value>メニュー</value>
</data>
<data name="users" xml:space="preserve">
<data name="Users" xml:space="preserve">
<value>ユーザー管理</value>
</data>
<data name="edit profile" xml:space="preserve">
<value>プロフィール編集</value>
</data>
<data name="user" xml:space="preserve">
<data name="User" xml:space="preserve">
<value>ユーザー</value>
</data>
<data name="view play data" xml:space="preserve">
<data name="View Play Data" xml:space="preserve">
<value>プレイデータ</value>
</data>
<data name="high scores" xml:space="preserve">
<value>演奏ゲーム</value>
</data>
<data name="dani dojo" xml:space="preserve">
<value>段位道場</value>
<data name="High Scores" xml:space="preserve">
<value>自己ベスト</value>
</data>
<data name="Show QR Code" xml:space="preserve">
<value>QRコード</value>
</data>
<data name="Manage Access Codes" xml:space="preserve">
<data name="Access Codes" xml:space="preserve">
<value>アクセスコード管理</value>
</data>
<data name="Change Password" xml:space="preserve">
@ -156,7 +153,7 @@
<data name="Welcome to TaikoWebUI!" xml:space="preserve">
<value>TaikoWebUIへようこそ!</value>
</data>
<data name="Song" xml:space="preserve">
<data name="Song Name" xml:space="preserve">
<value>曲名</value>
</data>
<data name="Level" xml:space="preserve">
@ -207,8 +204,8 @@
<data name="Total Donderful Combos" xml:space="preserve">
<value>ドンダフルコンボ回数</value>
</data>
<data name="Key_01" xml:space="preserve">
<value>自己ベスト</value>
<data name="Song List" xml:space="preserve">
<value>曲目リスト</value>
</data>
<data name="Hide" xml:space="preserve">
<value>隠す</value>
@ -229,9 +226,9 @@
<value>王冠</value>
</data>
<data name="No data." xml:space="preserve">
<value>データがありません</value>
<value>データがありません</value>
</data>
<data name="Key_02" xml:space="preserve">
<data name="Log In First" xml:space="preserve">
<value>"ユーザー管理"タブでログインしてください。</value>
</data>
<data name="Total Hits" xml:space="preserve">
@ -240,7 +237,7 @@
<data name="Soul Gauge" xml:space="preserve">
<value>魂ゲージ</value>
</data>
<data name="Songs" xml:space="preserve">
<data name="Course Songs" xml:space="preserve">
<value>課題曲</value>
</data>
<data name="Conditions" xml:space="preserve">
@ -252,8 +249,8 @@
<data name="Gold" xml:space="preserve">
<value>金合格</value>
</data>
<data name="Failed" xml:space="preserve">
<value>合格</value>
<data name="Not Cleared" xml:space="preserve">
<value>合格</value>
</data>
<data name="Pass" xml:space="preserve">
<value>合格</value>
@ -267,7 +264,7 @@
<data name="Stage" xml:space="preserve">
<value>曲目</value>
</data>
<data name="Key_03" xml:space="preserve">
<data name="Dani Dojo" xml:space="preserve">
<value>段位道場</value>
</data>
<data name="Profile" xml:space="preserve">
@ -378,4 +375,223 @@
<data name="DateFormat" xml:space="preserve">
<value>yyyy/MM/dd HH:mm:ss</value>
</data>
<data name="Generate Invite Code" xml:space="preserve">
<value>招待コード生成</value>
</data>
<data name="Register" xml:space="preserve">
<value>登録</value>
</data>
<data name="reset_password_confirm_dialog_1" xml:space="preserve">
<value>本当にこのユーザーのパスワードをリセットしますか?</value>
</data>
<data name="reset_password_confirm_dialog_2" xml:space="preserve">
<value>これにより、ユーザーの現在のパスワードは削除され、ユーザーは再度登録する必要があります。</value>
</data>
<data name="Log In" xml:space="preserve">
<value>ログイン</value>
</data>
<data name="Log Out" xml:space="preserve">
<value>ログアウト</value>
</data>
<data name="Play Data" xml:space="preserve">
<value>プレイデータ</value>
</data>
<data name="Add Access Code" xml:space="preserve">
<value>アクセスコード追加</value>
</data>
<data name="New Access Code" xml:space="preserve">
<value>新しいアクセスコード</value>
</data>
<data name="Delete" xml:space="preserve">
<value>削除</value>
</data>
<data name="Access Code" xml:space="preserve">
<value>アクセスコード</value>
</data>
<data name="Old Password" xml:space="preserve">
<value>旧パスワード</value>
</data>
<data name="New Password" xml:space="preserve">
<value>新しいパスワード</value>
</data>
<data name="Confirm New Password" xml:space="preserve">
<value>パスワードの再入力</value>
</data>
<data name="Dialog OK" xml:space="preserve">
<value>確認</value>
</data>
<data name="QR Code" xml:space="preserve">
<value>QRコード</value>
</data>
<data name="Chojin" xml:space="preserve">
<value>超人</value>
</data>
<data name="Eighth Dan" xml:space="preserve">
<value>八段</value>
</data>
<data name="Fifth Dan" xml:space="preserve">
<value>五段</value>
</data>
<data name="Fifth Kyuu" xml:space="preserve">
<value>五級</value>
</data>
<data name="First Dan" xml:space="preserve">
<value>一段</value>
</data>
<data name="First Kyuu" xml:space="preserve">
<value>一級</value>
</data>
<data name="Fourth Dan" xml:space="preserve">
<value>四段</value>
</data>
<data name="Fourth Kyuu" xml:space="preserve">
<value>四級</value>
</data>
<data name="Gaiden" xml:space="preserve">
<value>外伝</value>
</data>
<data name="Kuroto" xml:space="preserve">
<value>玄人</value>
</data>
<data name="Meijin" xml:space="preserve">
<value>名人</value>
</data>
<data name="Ninth Dan" xml:space="preserve">
<value>九段</value>
</data>
<data name="Second Dan" xml:space="preserve">
<value>二段</value>
</data>
<data name="Second Kyuu" xml:space="preserve">
<value>二級</value>
</data>
<data name="Seventh Dan" xml:space="preserve">
<value>七段</value>
</data>
<data name="Sixth Dan" xml:space="preserve">
<value>六段</value>
</data>
<data name="Tatsujin" xml:space="preserve">
<value>達人</value>
</data>
<data name="Tenth Dan" xml:space="preserve">
<value>十段</value>
</data>
<data name="Third Dan" xml:space="preserve">
<value>三段</value>
</data>
<data name="Third Kyuu" xml:space="preserve">
<value>三級</value>
</data>
<data name="Gold Full Combo" xml:space="preserve">
<value>金金合格</value>
</data>
<data name="Red Donderful Combo" xml:space="preserve">
<value>虹赤合格</value>
</data>
<data name="Red Full Combo" xml:space="preserve">
<value>金赤合格</value>
</data>
<data name="Gold Donderful Combo" xml:space="preserve">
<value>虹金合格</value>
</data>
<data name="Song Title / Artist" xml:space="preserve">
<value>曲名 / Artist</value>
</data>
<data name="Search by Title or Artist" xml:space="preserve">
<value>曲名・アーティスト名で検索</value>
</data>
<data name="Filter by Genre" xml:space="preserve">
<value>ジャンル検索</value>
</data>
<data name="Play History" xml:space="preserve">
<value>プレー履歴</value>
</data>
<data name="No Play History Found" xml:space="preserve">
<value>プレー履歴なし</value>
</data>
<data name="Password" xml:space="preserve">
<value>パスワード</value>
</data>
<data name="Settings" xml:space="preserve">
<value>設定</value>
</data>
<data name="Play Time" xml:space="preserve">
<value>プレー時間</value>
</data>
<data name="Rank" xml:space="preserve">
<value>ランク</value>
</data>
<data name="Difficulty" xml:space="preserve">
<value>難易度</value>
</data>
<data name="Song Number" xml:space="preserve">
<value>曲数</value>
</data>
<data name="Search by Title, Artist or Date" xml:space="preserve">
<value>曲名・アーティスト名・日付で検索</value>
</data>
<data name="Unregister" xml:space="preserve">
<value>登録解除</value>
</data>
<data name="and" xml:space="preserve">
<value>と</value>
</data>
<data name="other access code(s)" xml:space="preserve">
<value>他のアクセスコード</value>
</data>
<data name="Copy to Clipboard" xml:space="preserve">
<value>クリップボードにコピー</value>
</data>
<data name="Invite Code" xml:space="preserve">
<value>招待コード</value>
</data>
<data name="Error" xml:space="preserve">
<value>エラー</value>
</data>
<data name="Access Code is Required" xml:space="preserve">
<value>アクセスコードが必要です</value>
</data>
<data name="&quot;Invite Code (Optional)&quot;" xml:space="preserve">
<value>招待コード(任意)</value>
</data>
<data name="Last Play Date" xml:space="preserve">
<value>前回プレイデート</value>
</data>
<data name="Last Play Time(5 Min Around Credit End)" xml:space="preserve">
<value>前回プレー時間(クレジット終了5分以内)</value>
</data>
<data name="Password is Required" xml:space="preserve">
<value>パスワードが必要です</value>
</data>
<data name="Confirm Password" xml:space="preserve">
<value>パスワードの再入力</value>
</data>
<data name="Confirm Password is Required" xml:space="preserve">
<value>アクセスコードの再入力が必要です</value>
</data>
<data name="Unknown Error" xml:space="preserve">
<value>未知のエラー</value>
</data>
<data name="Success" xml:space="preserve">
<value>成功</value>
</data>
<data name="Ura Oni" xml:space="preserve">
<value>おに裏</value>
</data>
<data name="Oni" xml:space="preserve">
<value>おに</value>
</data>
<data name="Hard" xml:space="preserve">
<value>むずかしい</value>
</data>
<data name="Normal" xml:space="preserve">
<value>ふつう</value>
</data>
<data name="Easy" xml:space="preserve">
<value>かんたん</value>
</data>
<data name="Rows Per Page:" xml:space="preserve">
<value>1ページ当たりの行数</value>
</data>
</root>

View File

@ -120,4 +120,271 @@
<data name="DateFormat" xml:space="preserve">
<value>MM/dd/yyyy h:mm:ss tt</value>
</data>
<data name="Generate Invite Code" xml:space="preserve">
<value />
</data>
<data name="Register" xml:space="preserve">
<value />
</data>
<data name="Log In" xml:space="preserve">
<value />
</data>
<data name="Achievement Panel" xml:space="preserve">
<value />
</data>
<data name="Achievement Panel Difficulty" xml:space="preserve">
<value />
</data>
<data name="AI Battle Data" xml:space="preserve">
<value />
</data>
<data name="Bad" xml:space="preserve">
<value />
</data>
<data name="Best Crown" xml:space="preserve">
<value />
</data>
<data name="Best Rank" xml:space="preserve">
<value />
</data>
<data name="Log Out" xml:space="preserve">
<value />
</data>
<data name="Play Data" xml:space="preserve">
<value />
</data>
<data name="High Scores" xml:space="preserve">
<value />
</data>
<data name="Access Codes" xml:space="preserve">
<value />
</data>
<data name="Add Access Code" xml:space="preserve">
<value />
</data>
<data name="New Access Code" xml:space="preserve">
<value />
</data>
<data name="Delete" xml:space="preserve">
<value />
</data>
<data name="Access Code" xml:space="preserve">
<value />
</data>
<data name="Old Password" xml:space="preserve">
<value />
</data>
<data name="New Password" xml:space="preserve">
<value />
</data>
<data name="Confirm New Password" xml:space="preserve">
<value />
</data>
<data name="Dialog OK" xml:space="preserve">
<value />
</data>
<data name="QR Code" xml:space="preserve">
<value />
</data>
<data name="Fifth Kyuu" xml:space="preserve">
<value />
</data>
<data name="Fourth Kyuu" xml:space="preserve">
<value />
</data>
<data name="Third Kyuu" xml:space="preserve">
<value />
</data>
<data name="Second Kyuu" xml:space="preserve">
<value />
</data>
<data name="First Kyuu" xml:space="preserve">
<value />
</data>
<data name="First Dan" xml:space="preserve">
<value />
</data>
<data name="Second Dan" xml:space="preserve">
<value />
</data>
<data name="Third Dan" xml:space="preserve">
<value />
</data>
<data name="Fourth Dan" xml:space="preserve">
<value />
</data>
<data name="Fifth Dan" xml:space="preserve">
<value />
</data>
<data name="Sixth Dan" xml:space="preserve">
<value />
</data>
<data name="Seventh Dan" xml:space="preserve">
<value />
</data>
<data name="Eighth Dan" xml:space="preserve">
<value />
</data>
<data name="Ninth Dan" xml:space="preserve">
<value />
</data>
<data name="Tenth Dan" xml:space="preserve">
<value />
</data>
<data name="Kuroto" xml:space="preserve">
<value />
</data>
<data name="Meijin" xml:space="preserve">
<value />
</data>
<data name="Chojin" xml:space="preserve">
<value />
</data>
<data name="Tatsujin" xml:space="preserve">
<value />
</data>
<data name="Gaiden" xml:space="preserve">
<value />
</data>
<data name="Gold Full Combo" xml:space="preserve">
<value />
</data>
<data name="Red Full Combo" xml:space="preserve">
<value />
</data>
<data name="Red Donderful Combo" xml:space="preserve">
<value />
</data>
<data name="Gold Donderful Combo" xml:space="preserve">
<value />
</data>
<data name="Song List" xml:space="preserve">
<value />
</data>
<data name="Log In First" xml:space="preserve">
<value />
</data>
<data name="Dani Dojo" xml:space="preserve">
<value />
</data>
<data name="Course Songs" xml:space="preserve">
<value />
</data>
<data name="Song Name" xml:space="preserve">
<value />
</data>
<data name="Song Title / Artist" xml:space="preserve">
<value />
</data>
<data name="Search by Title or Artist" xml:space="preserve">
<value />
</data>
<data name="Filter by Genre" xml:space="preserve">
<value />
</data>
<data name="Play History" xml:space="preserve">
<value />
</data>
<data name="No Play History Found" xml:space="preserve">
<value />
</data>
<data name="Password" xml:space="preserve">
<value />
</data>
<data name="Settings" xml:space="preserve">
<value />
</data>
<data name="Play Time" xml:space="preserve">
<value />
</data>
<data name="Rank" xml:space="preserve">
<value />
</data>
<data name="Difficulty" xml:space="preserve">
<value />
</data>
<data name="Song Number" xml:space="preserve">
<value />
</data>
<data name="Search by Title, Artist or Date" xml:space="preserve">
<value />
</data>
<data name="Users" xml:space="preserve">
<value />
</data>
<data name="Dashboard" xml:space="preserve">
<value />
</data>
<data name="Unregister" xml:space="preserve">
<value />
</data>
<data name="and" xml:space="preserve">
<value />
</data>
<data name="other access code(s)" xml:space="preserve">
<value />
</data>
<data name="Copy to Clipboard" xml:space="preserve">
<value />
</data>
<data name="Invite Code" xml:space="preserve">
<value />
</data>
<data name="Error" xml:space="preserve">
<value />
</data>
<data name="View Play Data" xml:space="preserve">
<value />
</data>
<data name="Not Cleared" xml:space="preserve">
<value />
</data>
<data name="User" xml:space="preserve">
<value />
</data>
<data name="Access Code is Required" xml:space="preserve">
<value />
</data>
<data name="&quot;Invite Code (Optional)&quot;" xml:space="preserve">
<value />
</data>
<data name="Last Play Date" xml:space="preserve">
<value />
</data>
<data name="Last Play Time(5 Min Around Credit End)" xml:space="preserve">
<value />
</data>
<data name="Password is Required" xml:space="preserve">
<value />
</data>
<data name="Confirm Password" xml:space="preserve">
<value />
</data>
<data name="Confirm Password is Required" xml:space="preserve">
<value />
</data>
<data name="Unknown Error" xml:space="preserve">
<value />
</data>
<data name="Success" xml:space="preserve">
<value />
</data>
<data name="Ura Oni" xml:space="preserve">
<value />
</data>
<data name="Oni" xml:space="preserve">
<value />
</data>
<data name="Hard" xml:space="preserve">
<value />
</data>
<data name="Normal" xml:space="preserve">
<value />
</data>
<data name="Easy" xml:space="preserve">
<value />
</data>
<data name="Rows Per Page:" xml:space="preserve">
<value />
</data>
</root>

View File

@ -117,31 +117,28 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="dashboard" xml:space="preserve">
<data name="Dashboard" xml:space="preserve">
<value>主页</value>
</data>
<data name="users" xml:space="preserve">
<data name="Users" xml:space="preserve">
<value>用户管理</value>
</data>
<data name="edit profile" xml:space="preserve">
<value>编辑档案</value>
</data>
<data name="user" xml:space="preserve">
<data name="User" xml:space="preserve">
<value>用户</value>
</data>
<data name="view play data" xml:space="preserve">
<data name="View Play Data" xml:space="preserve">
<value>查看记录</value>
</data>
<data name="high scores" xml:space="preserve">
<value>演奏模式</value>
</data>
<data name="dani dojo" xml:space="preserve">
<value>段位道场</value>
<data name="High Scores" xml:space="preserve">
<value>最高分</value>
</data>
<data name="Show QR Code" xml:space="preserve">
<value>查看二维码</value>
</data>
<data name="Manage Access Codes" xml:space="preserve">
<data name="Access Codes" xml:space="preserve">
<value>管理访问码</value>
</data>
<data name="Change Password" xml:space="preserve">
@ -156,7 +153,7 @@
<data name="Welcome to TaikoWebUI!" xml:space="preserve">
<value>欢迎来到TaikoWebUI!</value>
</data>
<data name="Song" xml:space="preserve">
<data name="Song Name" xml:space="preserve">
<value>曲名</value>
</data>
<data name="Level" xml:space="preserve">
@ -207,8 +204,8 @@
<data name="Total Donderful Combos" xml:space="preserve">
<value>总完美连段次数</value>
</data>
<data name="Key_01" xml:space="preserve">
<value>最佳得分</value>
<data name="Song List" xml:space="preserve">
<value>曲目列表</value>
</data>
<data name="Hide" xml:space="preserve">
<value>隐藏</value>
@ -229,18 +226,18 @@
<value>王冠</value>
</data>
<data name="No data." xml:space="preserve">
<value>没有数据.</value>
<value>没有数据</value>
</data>
<data name="Key_02" xml:space="preserve">
<data name="Log In First" xml:space="preserve">
<value>"请先在用户管理那边登录</value>
</data>
<data name="Total Hits" xml:space="preserve">
<value>"总打击次数</value>
<value>总打击次数</value>
</data>
<data name="Soul Gauge" xml:space="preserve">
<value>魂条</value>
</data>
<data name="Songs" xml:space="preserve">
<data name="Course Songs" xml:space="preserve">
<value>课题曲</value>
</data>
<data name="Conditions" xml:space="preserve">
@ -252,8 +249,8 @@
<data name="Gold" xml:space="preserve">
<value>金合格</value>
</data>
<data name="Failed" xml:space="preserve">
<value>合格</value>
<data name="Not Cleared" xml:space="preserve">
<value>合格</value>
</data>
<data name="Pass" xml:space="preserve">
<value>合格</value>
@ -267,7 +264,7 @@
<data name="Stage" xml:space="preserve">
<value>曲目</value>
</data>
<data name="Key_03" xml:space="preserve">
<data name="Dani Dojo" xml:space="preserve">
<value>段位道场</value>
</data>
<data name="Profile" xml:space="preserve">
@ -378,4 +375,220 @@
<data name="DateFormat" xml:space="preserve">
<value>yyyy/M/d HH:mm:ss</value>
</data>
<data name="Generate Invite Code" xml:space="preserve">
<value>生成邀请码</value>
</data>
<data name="Register" xml:space="preserve">
<value>注册</value>
</data>
<data name="reset_password_confirm_dialog_1" xml:space="preserve">
<value>确定要为这位用户重置密码吗?</value>
</data>
<data name="reset_password_confirm_dialog_2" xml:space="preserve">
<value>重置密码后,用户需要再次注册</value>
</data>
<data name="Log In" xml:space="preserve">
<value>登录</value>
</data>
<data name="Log Out" xml:space="preserve">
<value>登出</value>
</data>
<data name="Play Data" xml:space="preserve">
<value>游玩数据</value>
</data>
<data name="Add Access Code" xml:space="preserve">
<value>新增访问码</value>
</data>
<data name="New Access Code" xml:space="preserve">
<value>新访问码</value>
</data>
<data name="Delete" xml:space="preserve">
<value>删除</value>
</data>
<data name="Access Code" xml:space="preserve">
<value>访问码</value>
</data>
<data name="Old Password" xml:space="preserve">
<value>现有密码</value>
</data>
<data name="New Password" xml:space="preserve">
<value>新密码</value>
</data>
<data name="Confirm New Password" xml:space="preserve">
<value>再次输入新密码</value>
</data>
<data name="Dialog OK" xml:space="preserve">
<value>确定</value>
</data>
<data name="QR Code" xml:space="preserve">
<value>二维码</value>
</data>
<data name="Chojin" xml:space="preserve">
<value>超人</value>
</data>
<data name="Eighth Dan" xml:space="preserve">
<value>八段</value>
</data>
<data name="Fifth Dan" xml:space="preserve">
<value>五段</value>
</data>
<data name="Fifth Kyuu" xml:space="preserve">
<value>五级</value>
</data>
<data name="First Dan" xml:space="preserve">
<value>一段</value>
</data>
<data name="First Kyuu" xml:space="preserve">
<value>一级</value>
</data>
<data name="Fourth Dan" xml:space="preserve">
<value>四段</value>
</data>
<data name="Fourth Kyuu" xml:space="preserve">
<value>四级</value>
</data>
<data name="Gaiden" xml:space="preserve">
<value>外传</value>
</data>
<data name="Kuroto" xml:space="preserve">
<value>玄人</value>
</data>
<data name="Meijin" xml:space="preserve">
<value>名人</value>
</data>
<data name="Ninth Dan" xml:space="preserve">
<value>九段</value>
</data>
<data name="Second Dan" xml:space="preserve">
<value>二段</value>
</data>
<data name="Second Kyuu" xml:space="preserve">
<value>二级</value>
</data>
<data name="Seventh Dan" xml:space="preserve">
<value>七段</value>
</data>
<data name="Sixth Dan" xml:space="preserve">
<value>六段</value>
</data>
<data name="Tatsujin" xml:space="preserve">
<value>达人</value>
</data>
<data name="Tenth Dan" xml:space="preserve">
<value>十段</value>
</data>
<data name="Third Dan" xml:space="preserve">
<value>三段</value>
</data>
<data name="Third Kyuu" xml:space="preserve">
<value>三级</value>
</data>
<data name="Gold Full Combo" xml:space="preserve">
<value>金金合格</value>
</data>
<data name="Red Donderful Combo" xml:space="preserve">
<value>虹赤合格</value>
</data>
<data name="Red Full Combo" xml:space="preserve">
<value>金赤合格</value>
</data>
<data name="Gold Donderful Combo" xml:space="preserve">
<value>虹金合格</value>
</data>
<data name="Song Title / Artist" xml:space="preserve">
<value>曲名 / Artist</value>
</data>
<data name="Search by Title or Artist" xml:space="preserve">
<value>曲名/Artist名搜索</value>
</data>
<data name="Filter by Genre" xml:space="preserve">
<value>分类过滤</value>
</data>
<data name="Play History" xml:space="preserve">
<value>游玩历史</value>
</data>
<data name="No Play History Found" xml:space="preserve">
<value>沒有游玩历史</value>
</data>
<data name="Password" xml:space="preserve">
<value>密码</value>
</data>
<data name="Settings" xml:space="preserve">
<value>设定</value>
</data>
<data name="Play Time" xml:space="preserve">
<value>游玩时间</value>
</data>
<data name="Rank" xml:space="preserve">
<value>评价</value>
</data>
<data name="Difficulty" xml:space="preserve">
<value>难度</value>
</data>
<data name="Song Number" xml:space="preserve">
<value>曲数</value>
</data>
<data name="Search by Title, Artist or Date" xml:space="preserve">
<value>曲名/Artist名/日期搜索</value>
</data>
<data name="Unregister" xml:space="preserve">
<value>注销账号</value>
</data>
<data name="and" xml:space="preserve">
<value>和</value>
</data>
<data name="other access code(s)" xml:space="preserve">
<value>个其他访问码</value>
</data>
<data name="Copy to Clipboard" xml:space="preserve">
<value>复制到粘贴板</value>
</data>
<data name="Invite Code" xml:space="preserve">
<value>邀请码</value>
</data>
<data name="Error" xml:space="preserve">
<value>错误</value>
</data>
<data name="Access Code is Required" xml:space="preserve">
<value>访问码为必填项</value>
</data>
<data name="&quot;Invite Code (Optional)&quot;" xml:space="preserve">
<value>邀请码(可选)</value>
</data>
<data name="Last Play Time(5 Min Around Credit End)" xml:space="preserve">
<value>最后游玩时间(游戏结束5分钟内)</value>
</data>
<data name="Password is Required" xml:space="preserve">
<value>密码为必填项</value>
</data>
<data name="Confirm Password" xml:space="preserve">
<value>再次输入密码</value>
</data>
<data name="Confirm Password is Required" xml:space="preserve">
<value>再次输入密码为必填项</value>
</data>
<data name="Unknown Error" xml:space="preserve">
<value>不明错误</value>
</data>
<data name="Success" xml:space="preserve">
<value>成功</value>
</data>
<data name="Ura Oni" xml:space="preserve">
<value>里譜面</value>
</data>
<data name="Oni" xml:space="preserve">
<value>魔王</value>
</data>
<data name="Hard" xml:space="preserve">
<value>困难</value>
</data>
<data name="Normal" xml:space="preserve">
<value>普通</value>
</data>
<data name="Easy" xml:space="preserve">
<value>简单</value>
</data>
<data name="Rows Per Page:" xml:space="preserve">
<value>每页行数</value>
</data>
</root>

View File

@ -117,32 +117,29 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="dashboard" xml:space="preserve">
<data name="Dashboard" xml:space="preserve">
<value>首頁</value>
</data>
<data name="users" xml:space="preserve">
<data name="Users" xml:space="preserve">
<value>管理用戶</value>
</data>
<data name="edit profile" xml:space="preserve">
<value>編輯檔案</value>
</data>
<data name="user" xml:space="preserve">
<data name="User" xml:space="preserve">
<value>用戶</value>
</data>
<data name="view play data" xml:space="preserve">
<data name="View Play Data" xml:space="preserve">
<value>查看記錄</value>
</data>
<data name="high scores" xml:space="preserve">
<value>演奏模式</value>
</data>
<data name="dani dojo" xml:space="preserve">
<value>段位道場</value>
<data name="High Scores" xml:space="preserve">
<value>最高分</value>
</data>
<data name="Show QR Code" xml:space="preserve">
<value>查看QR Code</value>
</data>
<data name="Manage Access Codes" xml:space="preserve">
<value>管理Access Codes</value>
<data name="Access Codes" xml:space="preserve">
<value>管理訪問碼</value>
</data>
<data name="Change Password" xml:space="preserve">
<value>更改密碼</value>
@ -156,7 +153,7 @@
<data name="Welcome to TaikoWebUI!" xml:space="preserve">
<value>歡迎來到TaikoWebUI!</value>
</data>
<data name="Song" xml:space="preserve">
<data name="Song Name" xml:space="preserve">
<value>曲名</value>
</data>
<data name="Level" xml:space="preserve">
@ -207,8 +204,8 @@
<data name="Total Donderful Combos" xml:space="preserve">
<value>總全良次數</value>
</data>
<data name="Key_01" xml:space="preserve">
<value>最佳得分</value>
<data name="Song List" xml:space="preserve">
<value>曲目列表</value>
</data>
<data name="Hide" xml:space="preserve">
<value>隱藏</value>
@ -229,18 +226,18 @@
<value>皇冠</value>
</data>
<data name="No data." xml:space="preserve">
<value>沒有數據.</value>
<value>沒有數據</value>
</data>
<data name="Key_02" xml:space="preserve">
<data name="Log In First" xml:space="preserve">
<value>"請先在管理用戶那邊登入</value>
</data>
<data name="Total Hits" xml:space="preserve">
<value>"總打擊次數</value>
<value>總打擊次數</value>
</data>
<data name="Soul Gauge" xml:space="preserve">
<value>魂條</value>
</data>
<data name="Songs" xml:space="preserve">
<data name="Course Songs" xml:space="preserve">
<value>課題曲</value>
</data>
<data name="Conditions" xml:space="preserve">
@ -252,8 +249,8 @@
<data name="Gold" xml:space="preserve">
<value>金合格</value>
</data>
<data name="Failed" xml:space="preserve">
<value>合格</value>
<data name="Not Cleared" xml:space="preserve">
<value>合格</value>
</data>
<data name="Pass" xml:space="preserve">
<value>合格</value>
@ -267,7 +264,7 @@
<data name="Stage" xml:space="preserve">
<value>曲目</value>
</data>
<data name="Key_03" xml:space="preserve">
<data name="Dani Dojo" xml:space="preserve">
<value>段位道埸</value>
</data>
<data name="Profile" xml:space="preserve">
@ -378,4 +375,220 @@
<data name="DateFormat" xml:space="preserve">
<value>yyyy/M/d HH:mm:ss</value>
</data>
<data name="Generate Invite Code" xml:space="preserve">
<value>生成邀請碼</value>
</data>
<data name="Register" xml:space="preserve">
<value>注冊</value>
</data>
<data name="reset_password_confirm_dialog_1" xml:space="preserve">
<value>確定要為這位用戶重置密碼嗎?</value>
</data>
<data name="reset_password_confirm_dialog_2" xml:space="preserve">
<value>重置密碼后,用戶需要再次注冊</value>
</data>
<data name="Log In" xml:space="preserve">
<value>登錄</value>
</data>
<data name="Log Out" xml:space="preserve">
<value>登出</value>
</data>
<data name="Play Data" xml:space="preserve">
<value>游玩數據</value>
</data>
<data name="Add Access Code" xml:space="preserve">
<value>新增訪問碼</value>
</data>
<data name="New Access Code" xml:space="preserve">
<value>新訪問碼</value>
</data>
<data name="Delete" xml:space="preserve">
<value>删除</value>
</data>
<data name="Access Code" xml:space="preserve">
<value>訪問碼</value>
</data>
<data name="Old Password" xml:space="preserve">
<value>現有密碼</value>
</data>
<data name="New Password" xml:space="preserve">
<value>新密碼</value>
</data>
<data name="Confirm New Password" xml:space="preserve">
<value>再次輸入新密碼</value>
</data>
<data name="Dialog OK" xml:space="preserve">
<value>確認</value>
</data>
<data name="QR Code" xml:space="preserve">
<value>查看QR Code</value>
</data>
<data name="Chojin" xml:space="preserve">
<value>超人</value>
</data>
<data name="Eighth Dan" xml:space="preserve">
<value>八段</value>
</data>
<data name="Gaiden" xml:space="preserve">
<value>外傳</value>
</data>
<data name="Fourth Kyuu" xml:space="preserve">
<value>四級</value>
</data>
<data name="Fourth Dan" xml:space="preserve">
<value>四段</value>
</data>
<data name="First Kyuu" xml:space="preserve">
<value>一級</value>
</data>
<data name="First Dan" xml:space="preserve">
<value>一段</value>
</data>
<data name="Fifth Kyuu" xml:space="preserve">
<value>五級</value>
</data>
<data name="Fifth Dan" xml:space="preserve">
<value>五段</value>
</data>
<data name="Kuroto" xml:space="preserve">
<value>玄人</value>
</data>
<data name="Meijin" xml:space="preserve">
<value>名人</value>
</data>
<data name="Ninth Dan" xml:space="preserve">
<value>九段</value>
</data>
<data name="Second Dan" xml:space="preserve">
<value>二段</value>
</data>
<data name="Second Kyuu" xml:space="preserve">
<value>二級</value>
</data>
<data name="Seventh Dan" xml:space="preserve">
<value>七段</value>
</data>
<data name="Sixth Dan" xml:space="preserve">
<value>六段</value>
</data>
<data name="Tatsujin" xml:space="preserve">
<value>達人</value>
</data>
<data name="Tenth Dan" xml:space="preserve">
<value>十段</value>
</data>
<data name="Third Dan" xml:space="preserve">
<value>三段</value>
</data>
<data name="Third Kyuu" xml:space="preserve">
<value>三級</value>
</data>
<data name="Gold Full Combo" xml:space="preserve">
<value>金金合格</value>
</data>
<data name="Red Donderful Combo" xml:space="preserve">
<value>虹赤合格</value>
</data>
<data name="Red Full Combo" xml:space="preserve">
<value>金赤合格</value>
</data>
<data name="Gold Donderful Combo" xml:space="preserve">
<value>虹金合格</value>
</data>
<data name="Song Title / Artist" xml:space="preserve">
<value>曲名 / Artist</value>
</data>
<data name="Search by Title or Artist" xml:space="preserve">
<value>曲名/Artist名搜索</value>
</data>
<data name="Filter by Genre" xml:space="preserve">
<value>分類過濾</value>
</data>
<data name="Play History" xml:space="preserve">
<value>游玩歷史</value>
</data>
<data name="No Play History Found" xml:space="preserve">
<value>没有游玩歷史</value>
</data>
<data name="Password" xml:space="preserve">
<value>密碼</value>
</data>
<data name="Settings" xml:space="preserve">
<value>設定</value>
</data>
<data name="Play Time" xml:space="preserve">
<value>游玩時間</value>
</data>
<data name="Rank" xml:space="preserve">
<value>評價</value>
</data>
<data name="Difficulty" xml:space="preserve">
<value>難度</value>
</data>
<data name="Song Number" xml:space="preserve">
<value>曲數</value>
</data>
<data name="Search by Title, Artist or Date" xml:space="preserve">
<value>曲名/Artist名/日期搜索</value>
</data>
<data name="Unregister" xml:space="preserve">
<value>注銷賬號</value>
</data>
<data name="and" xml:space="preserve">
<value>和</value>
</data>
<data name="other access code(s)" xml:space="preserve">
<value>個其他訪問碼</value>
</data>
<data name="Copy to Clipboard" xml:space="preserve">
<value>複製到粘貼板</value>
</data>
<data name="Invite Code" xml:space="preserve">
<value>邀請碼</value>
</data>
<data name="Error" xml:space="preserve">
<value> 錯誤</value>
</data>
<data name="Access Code is Required" xml:space="preserve">
<value>訪問碼為必填項</value>
</data>
<data name="&quot;Invite Code (Optional)&quot;" xml:space="preserve">
<value>邀請碼(可選)</value>
</data>
<data name="Last Play Time(5 Min Around Credit End)" xml:space="preserve">
<value>最後游玩時間(游戲結束5分鐘内)</value>
</data>
<data name="Password is Required" xml:space="preserve">
<value>密碼為必填項</value>
</data>
<data name="Confirm Password" xml:space="preserve">
<value>再次輸入密碼</value>
</data>
<data name="Confirm Password is Required" xml:space="preserve">
<value>再次輸入密碼為必填項</value>
</data>
<data name="Unknown Error" xml:space="preserve">
<value>不明錯誤</value>
</data>
<data name="Success" xml:space="preserve">
<value>成功</value>
</data>
<data name="Ura Oni" xml:space="preserve">
<value>裏譜面</value>
</data>
<data name="Oni" xml:space="preserve">
<value>魔王</value>
</data>
<data name="Hard" xml:space="preserve">
<value>困難</value>
</data>
<data name="Normal" xml:space="preserve">
<value>普通</value>
</data>
<data name="Easy" xml:space="preserve">
<value>簡單</value>
</data>
<data name="Rows Per Page:" xml:space="preserve">
<value>每頁行數</value>
</data>
</root>

View File

@ -1,43 +1,36 @@
@page "/Users/{baid:int}/AccessCode"
@inject HttpClient Client
@inject IDialogService DialogService
@inject LoginService LoginService
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@inject TaikoWebUI.Utilities.StringUtil StringUtil;
@inject Utilities.StringUtil StringUtil;
@if (response is not null)
{
@if ((LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) || User is null)
{
if (!LoginService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
}
else
{
@if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin))) {
NavigationManager.NavigateTo(!AuthService.IsLoggedIn ? "/Login" : "/");
} else if (User is null) {
// Loading ...
<MudContainer Style="display:flex;margin:50px 0;align-items:center;justify-content:center;">
<MudProgressCircular Indeterminate="true" Size="Size.Large" Color="Color.Primary" />
</MudContainer>
} else {
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-2"></MudBreadcrumbs>
<MudText Typo="Typo.h4">Access Codes</MudText>
<MudText Typo="Typo.h4">@Localizer["Access Codes"]</MudText>
<MudGrid Class="my-4 pb-10">
<MudItem xs="12">
<MudCard Outlined="true" Class="mb-6">
<MudCardContent>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudText Typo="Typo.h6">Add Access Code</MudText>
<MudText Typo="Typo.h6">@Localizer["Add Access Code"]</MudText>
<MudForm @ref="bindAccessCodeForm">
<MudGrid Spacing="2" Class="mt-4">
<MudItem xs="12" md="10">
<MudTextField @bind-value="inputAccessCode" InputType="InputType.Text" T="string"
FullWidth="true" Required="@true" RequiredError="Access Code is required" Variant="Variant.Outlined" Margin="Margin.Dense"
Label="New Access Code" />
Label=@Localizer["New Access Code"] />
</MudItem>
<MudItem xs="12" md="2">
<MudButton OnClick="OnBind" FullWidth="true" StartIcon="@Icons.Material.Filled.AddCard" Color="Color.Primary" Variant="Variant.Filled" Class="mt-1">Add</MudButton>
<MudButton OnClick="OnBind" FullWidth="true" StartIcon="@Icons.Material.Filled.AddCard" Color="Color.Primary" Variant="Variant.Filled" Class="mt-1">@Localizer["Add"]</MudButton>
</MudItem>
</MudGrid>
</MudForm>
@ -50,7 +43,7 @@
<MudCardContent>
<MudGrid Spacing="3" Class="pb-2">
<MudItem xs="12">
<MudText Typo="Typo.h6">Access Codes</MudText>
<MudText Typo="Typo.h6">@Localizer["Access Code"]</MudText>
</MudItem>
@for (var idx = 0; idx < User.AccessCodes.Count; idx++)
{
@ -83,12 +76,4 @@
</MudCard>
</MudItem>
</MudGrid>
}
}
else
{
<MudContainer Style="display:flex;margin:50px 0;align-items:center;justify-content:center;">
<MudProgressCircular Indeterminate="true" Size="Size.Large" Color="Color.Primary" />
</MudContainer>
}

View File

@ -10,9 +10,8 @@ public partial class AccessCode
private string inputAccessCode = "";
private MudForm bindAccessCodeForm = default!;
private User? User { get; set; } = new();
private User? User { get; set; }
private DashboardResponse? response;
private UserSetting? userSetting;
private readonly List<BreadcrumbItem> breadcrumbs = new();
@ -24,32 +23,28 @@ public partial class AccessCode
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
if (LoginService.IsLoggedIn && !LoginService.IsAdmin)
if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{
breadcrumbs.Add(new BreadcrumbItem("Dashboard", href: "/"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
}
else
{
breadcrumbs.Add(new BreadcrumbItem("Users", href: "/Users"));
};
breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users"));
}
breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem("Access Codes", href: $"/Users/{Baid}/AccessCode", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Access Codes"], href: $"/Users/{Baid}/AccessCode", disabled: false));
}
private async Task InitializeUser()
{
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
LoginService.ResetLoggedInUser(response);
if (LoginService.IsAdmin || !LoginService.LoginRequired)
if (!AuthService.LoginRequired)
{
if (response is not null)
{
User = response.Users.FirstOrDefault(u => u.Baid == Baid);
var users = await Client.GetFromJsonAsync<List<User>>("api/Users");
if (users != null) User = users.FirstOrDefault(u => u.Baid == Baid);
}
}
else if (LoginService.IsLoggedIn)
else if (AuthService.IsLoggedIn)
{
User = LoginService.GetLoggedInUser();
User = await Client.GetFromJsonAsync<User>($"api/Users/{Baid}");
}
}
@ -61,7 +56,7 @@ public partial class AccessCode
{ x => x.AccessCode, accessCode }
};
var dialog = DialogService.Show<AccessCodeDeleteConfirmDialog>("Delete Access Code", parameters);
var dialog = await DialogService.ShowAsync<AccessCodeDeleteConfirmDialog>("Delete Access Code", parameters);
var result = await dialog.Result;
if (result.Canceled) return;
@ -72,9 +67,8 @@ public partial class AccessCode
private async Task OnBind()
{
if (response != null)
{
var result = await LoginService.BindAccessCode(inputAccessCode.ToUpper().Trim(), response.Users.First(u => u.Baid == Baid), Client);
if (User == null) return;
var result = await AuthService.BindAccessCode(inputAccessCode.ToUpper().Trim(), User);
switch (result)
{
case 0:
@ -122,5 +116,4 @@ public partial class AccessCode
break;
}
}
}
}

View File

@ -1,17 +1,17 @@
@inject HttpClient Client
@inject IDialogService DialogService
@inject LoginService LoginService
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@page "/ChangePassword"
@if (LoginService.OnlyAdmin || !LoginService.LoginRequired)
@if (AuthService.OnlyAdmin || !AuthService.LoginRequired)
{
NavigationManager.NavigateTo("/");
}
else
{
if (LoginService.IsLoggedIn)
if (AuthService.IsLoggedIn)
{
<MudContainer>
<MudGrid Justify="Justify.Center">
@ -19,24 +19,24 @@ else
<MudCard Elevation="0" Outlined="true">
<MudCardContent>
<MudForm @ref="changePasswordForm">
<MudText Typo="Typo.h5" Class="mb-4">Change Password</MudText>
<MudText Typo="Typo.h5" Class="mb-4">@Localizer["Change Password"]</MudText>
<div style="display:flex;flex-direction:column;gap:15px;">
<MudTextField @bind-value="cardNum" InputType="InputType.Text" T="string"
FullWidth="true" Required="@true" RequiredError="Access code is required"
Label="Access Code" Variant="Variant.Outlined" Margin="Margin.Dense" />
Label=@Localizer["Access Code"] Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudTextField @bind-Value="oldPassword" InputType="InputType.Password"
T="string" FullWidth="true" Required="@true"
RequiredError="Old Password is required"
Label="Old Password" Variant="Variant.Outlined" Margin="Margin.Dense" />
Label=@Localizer["Old Password"] Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudTextField @bind-Value="newPassword" InputType="InputType.Password"
T="string" FullWidth="true" Required="@true"
RequiredError="Password is required"
Label="New Password" Variant="Variant.Outlined" Margin="Margin.Dense" />
Label=@Localizer["New Password"] Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudTextField @bind-Value="confirmNewPassword" InputType="InputType.Password"
T="string" FullWidth="true" Required="@true"
RequiredError="Confirm password is required"
Label="Confirm New Password" Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudButton OnClick="OnChangePassword" FullWidth="true" Class="mt-3" StartIcon="@Icons.Material.Filled.Edit" Color="Color.Primary" Variant="Variant.Filled">Change password</MudButton>
Label=@Localizer["Confirm New Password"] Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudButton OnClick="OnChangePassword" FullWidth="true" Class="mt-3" StartIcon="@Icons.Material.Filled.Edit" Color="Color.Primary" Variant="Variant.Filled">@Localizer["Change Password"]</MudButton>
</div>
</MudForm>
</MudCardContent>

View File

@ -8,57 +8,63 @@ public partial class ChangePassword
private string newPassword = "";
private string oldPassword = "";
private DashboardResponse? response;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
}
private async Task OnChangePassword()
{
if (response != null)
{
var result = await LoginService.ChangePassword(cardNum, oldPassword, newPassword, confirmNewPassword,
response, Client);
var result = await AuthService.ChangePassword(cardNum, oldPassword, newPassword, confirmNewPassword);
switch (result)
{
case 0:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
"Only admin can log in.",
"Ok");
Localizer["Dialog OK"]);
NavigationManager.NavigateTo("/Users");
break;
case 1:
await DialogService.ShowMessageBox(
"Success",
Localizer["Success"],
"Password changed successfully.",
"Ok");
Localizer["Dialog OK"]);
NavigationManager.NavigateTo("/Users");
break;
case 2:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
"Confirm new password is not the same as new password.",
"Ok");
Localizer["Dialog OK"]);
break;
case 3:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
(MarkupString)
"Card number not found.<br />Please play one game with this card number to register it.",
"Ok");
Localizer["Dialog OK"]);
break;
case 4:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
(MarkupString)
"Old password is wrong!",
"Ok");
Localizer["Dialog OK"]);
break;
case 5:
await DialogService.ShowMessageBox(
Localizer["Error"],
(MarkupString)
"Card number not registered.<br />Please use register button to create a password first.",
Localizer["Dialog OK"]);
break;
case 6:
await DialogService.ShowMessageBox(
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"]);
break;
}
}
}
}

View File

@ -1,14 +1,14 @@
@inject IGameDataService GameDataService
@inject HttpClient Client
@inject LoginService LoginService
@inject IJSRuntime JSRuntime
@inject AuthService AuthService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
@page "/Users/{baid:int}/DaniDojo"
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin)))
@if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{
if (!LoginService.IsLoggedIn)
if (!AuthService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
@ -20,7 +20,7 @@
else
{
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-2"></MudBreadcrumbs>
<MudText Typo="Typo.h4">@Localizer["Key_03"]</MudText>
<MudText Typo="Typo.h4">@Localizer["Dani Dojo"]</MudText>
<MudGrid Class="my-4 pb-10">
<MudItem xs="12">
<MudPaper Elevation="0" Outlined="true">
@ -58,7 +58,7 @@ else
<MudCard Outlined="true">
<MudCardHeader Class="pb-0">
<CardHeaderContent>
<MudText Typo="Typo.h6">@Localizer["Key_01"]</MudText>
<MudText Typo="Typo.h6">@Localizer["Score"]</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="d-flex py-10" Style="justify-content:center">
@ -119,7 +119,7 @@ else
</MudItem>
</MudGrid>
<MudText Typo="Typo.h5" Class="mt-10 mb-4">@Localizer["Songs"]</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-4">@Localizer["Course Songs"]</MudText>
<MudGrid>
<MudItem xs="12">
<MudGrid Class="d-block">
@ -214,7 +214,7 @@ else
var redRequirement = GetSoulGauge(danData, false);
var goldRequirement = GetSoulGauge(danData, true);
var barClass = "bar-default";
var resultText = @Localizer["Failed"];
var resultText = @Localizer["Not Cleared"];
}
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2" Style="font-weight:bold;">@Localizer["Result"]</MudText>
@ -284,7 +284,7 @@ else
var redRequirement = border.RedBorderTotal;
var goldRequirement = border.GoldBorderTotal;
var barClass = "bar-default";
var resultText = @Localizer["Failed"];
var resultText = @Localizer["Not Cleared"];
}
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2" Style="font-weight:bold;">@Localizer["Result"]</MudText>
@ -370,7 +370,7 @@ else
var redRequirement = GetSongBorderCondition(border, songNumber, false);
var goldRequirement = GetSongBorderCondition(border, songNumber, true);
var barClass = "bar-default";
var resultText = @Localizer["Failed"];
var resultText = Localizer["Not Cleared"];
<MudItem xs="12" md="4">
<MudCard Outlined="true">
@ -381,7 +381,7 @@ else
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.subtitle2" Style="font-weight:bold;">@Localizer["Result"]</MudText>
@if (bestDataMap.TryGetValue(danId, out var danBestData))
@if (bestDataMap.TryGetValue(danId, out var danBestData) && (danBestData.DanBestStageDataList.Count > songNumber))
{
var bestData = GetSongBestFromData((DanConditionType)border.OdaiType, danBestData, songNumber);
if ((DanConditionType)border.OdaiType is DanConditionType.BadCount or DanConditionType.OkCount)
@ -389,14 +389,15 @@ else
if (bestData <= redRequirement)
{
barClass = "bar-pass-red";
resultText = @Localizer["Pass"];
resultText = Localizer["Pass"];
}
if (bestData <= goldRequirement)
{
barClass = "bar-pass-gold";
resultText = @Localizer["Gold"];
resultText = Localizer["Gold"];
}
var resultValue = redRequirement - bestData;
if (bestData >= redRequirement) resultValue = 0;
@ -410,13 +411,13 @@ else
if (bestData >= redRequirement)
{
barClass = "bar-pass-red";
resultText = @Localizer["Pass"];
resultText = Localizer["Pass"];
}
if (bestData >= goldRequirement)
{
barClass = "bar-pass-gold";
resultText = @Localizer["Gold"];
resultText = Localizer["Gold"];
}
<MudProgressLinear Class="@barClass" Rounded="true" Size="Size.Large" Max="@(goldRequirement > 0 ? goldRequirement : 1)" Value="@(goldRequirement > 0 ? bestData : 1)">
@ -426,7 +427,6 @@ else
<MudText Typo="Typo.caption">@resultText</MudText>
</MudStack>
}
}
else
{

View File

@ -25,54 +25,54 @@ public partial class DaniDojo
.Sort((stageData, otherStageData) => stageData.SongNumber.CompareTo(otherStageData.SongNumber)));
bestDataMap = response.DanBestDataList.ToDictionary(data => data.DanId);
CurrentLanguage = await JSRuntime.InvokeAsync<string>("blazorCulture.get");
CurrentLanguage = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
if (LoginService.IsLoggedIn && !LoginService.IsAdmin)
if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{
breadcrumbs.Add(new BreadcrumbItem("Dashboard", href: "/"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
}
else
{
breadcrumbs.Add(new BreadcrumbItem("Users", href: "/Users"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users"));
};
breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem("Dani Dojo", href: $"/Users/{Baid}/DaniDojo", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dani Dojo"], href: $"/Users/{Baid}/DaniDojo", disabled: false));
}
private static string GetDanClearStateString(DanClearState danClearState)
private string GetDanClearStateString(DanClearState danClearState)
{
return danClearState switch
{
DanClearState.NotClear => "Not Cleared",
DanClearState.RedNormalClear => "Red Clear",
DanClearState.RedFullComboClear => "Red Full Combo",
DanClearState.RedPerfectClear => "Red Donderful Combo",
DanClearState.GoldNormalClear => "Gold Clear",
DanClearState.GoldFullComboClear => "Gold Full Combo",
DanClearState.GoldPerfectClear => "Gold Donderful Combo",
DanClearState.NotClear => Localizer["Not Cleared"],
DanClearState.RedNormalClear => Localizer["Red"],
DanClearState.RedFullComboClear => Localizer["Red Full Combo"],
DanClearState.RedPerfectClear => Localizer["Red Donderful Combo"],
DanClearState.GoldNormalClear => Localizer["Gold"],
DanClearState.GoldFullComboClear => Localizer["Gold Full Combo"],
DanClearState.GoldPerfectClear => Localizer["Gold Donderful Combo"],
_ => ""
};
}
private static string GetDanRequirementString(DanConditionType danConditionType)
private string GetDanRequirementString(DanConditionType danConditionType)
{
return danConditionType switch
{
DanConditionType.TotalHitCount => "Total Hits",
DanConditionType.GoodCount => "Good Hits",
DanConditionType.OkCount => "OK Hits",
DanConditionType.BadCount => "Bad Hits",
DanConditionType.SoulGauge => "Soul Gauge",
DanConditionType.DrumrollCount => "Drumroll Hits",
DanConditionType.Score => "Score",
DanConditionType.ComboCount => "MAX Combo",
DanConditionType.TotalHitCount => Localizer["Total Hits"],
DanConditionType.GoodCount => Localizer["Good"],
DanConditionType.OkCount => Localizer["OK"],
DanConditionType.BadCount => Localizer["Bad"],
DanConditionType.SoulGauge => Localizer["Soul Gauge"],
DanConditionType.DrumrollCount => Localizer["Drumroll"],
DanConditionType.Score => Localizer["Score"],
DanConditionType.ComboCount => Localizer["MAX Combo"],
_ => ""
};
}
private static string GetDanRequirementTitle(DanData.OdaiBorder data)
private string GetDanRequirementTitle(DanData.OdaiBorder data)
{
var danConditionType = (DanConditionType)data.OdaiType;
@ -98,6 +98,7 @@ public partial class DaniDojo
private static uint GetSongBestFromData(DanConditionType type, DanBestData data, int songNumber)
{
songNumber.Throw().IfOutOfRange(0, 2);
return type switch
{
DanConditionType.SoulGauge => throw new ArgumentException("Soul gauge should not be here"),
@ -134,30 +135,30 @@ public partial class DaniDojo
};
}
private static string GetDanTitle(string title)
private string GetDanTitle(string title)
{
return title switch
{
"5kyuu" => "Fifth Kyuu",
"4kyuu" => "Fourth Kyuu",
"3kyuu" => "Third Kyuu",
"2kyuu" => "Second Kyuu",
"1kyuu" => "First Kyuu",
"1dan" => "First Dan",
"2dan" => "Second Dan",
"3dan" => "Third Dan",
"4dan" => "Fourth Dan",
"5dan" => "Fifth Dan",
"6dan" => "Sixth Dan",
"7dan" => "Seventh Dan",
"8dan" => "Eighth Dan",
"9dan" => "Ninth Dan",
"10dan" => "Tenth Dan",
"11dan" => "Kuroto",
"12dan" => "Meijin",
"13dan" => "Chojin",
"14dan" => "Tatsujin",
"15dan" => "Gaiden",
"5kyuu" => Localizer["Fifth Kyuu"],
"4kyuu" => Localizer["Fourth Kyuu"],
"3kyuu" => Localizer["Third Kyuu"],
"2kyuu" => Localizer["Second Kyuu"],
"1kyuu" => Localizer["First Kyuu"],
"1dan" => Localizer["First Dan"],
"2dan" => Localizer["Second Dan"],
"3dan" => Localizer["Third Dan"],
"4dan" => Localizer["Fourth Dan"],
"5dan" => Localizer["Fifth Dan"],
"6dan" => Localizer["Sixth Dan"],
"7dan" => Localizer["Seventh Dan"],
"8dan" => Localizer["Eighth Dan"],
"9dan" => Localizer["Ninth Dan"],
"10dan" => Localizer["Tenth Dan"],
"11dan" => Localizer["Kuroto"],
"12dan" => Localizer["Meijin"],
"13dan" => Localizer["Chojin"],
"14dan" => Localizer["Tatsujin"],
"15dan" => Localizer["Gaiden"],
_ => ""
};
}

View File

@ -1,6 +1,6 @@
@page "/"
<MudText Typo="Typo.h4">@Localizer["dashboard"]</MudText>
<MudText Typo="Typo.h4">@Localizer["Dashboard"]</MudText>
<MudText Class="mt-8">
@Localizer["Welcome to TaikoWebUI!"]

View File

@ -16,7 +16,7 @@
<MudText>
<code>
<pre>
@String.Format("{0:0000 0000 0000 0000 0000}", (Int64.Parse(AccessCode)))
@AccessCode
</pre>
</code>
</MudText>

View File

@ -1,10 +1,12 @@
namespace TaikoWebUI.Pages.Dialogs;
using System.Net.Http.Headers;
using Blazored.LocalStorage;
namespace TaikoWebUI.Pages.Dialogs;
public partial class AccessCodeDeleteConfirmDialog
{
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public User User { get; set; } = new();
@ -12,6 +14,13 @@ public partial class AccessCodeDeleteConfirmDialog
[Parameter]
public string AccessCode { get; set; } = "";
[Inject]
public ILocalStorageService LocalStorage { get; set; } = null!;
[Inject]
public AuthService AuthService { get; set; } = null!;
private void Cancel() => MudDialog.Cancel();
private async Task DeleteAccessCode()

View File

@ -1,6 +1,7 @@
@using TaikoWebUI.Shared.Models
@using System.Collections.Immutable
@inject IGameDataService GameDataService
@inject IJSRuntime Js
<MudDialog>
<DialogContent>
@ -38,7 +39,7 @@
}
</RowTemplate>
<PagerContent>
<MudTablePager />
<MudTablePager RowsPerPageString=@Localizer["Rows Per Page"] />
</PagerContent>
</MudTable>
<MudText Class="mt-4 d-block" Typo="Typo.caption"> <b>@Localizer["Selected Title:"]</b> @selectedTitle?.TitleName</MudText>
@ -64,6 +65,9 @@
[Parameter]
public bool AllowFreeProfileEditing { get; set; }
[Parameter]
public List<uint> TitleUniqueIdList { get; set; } = new();
private IEnumerable<Title> titles = new List<Title>();
private Title? selectedTitle;
@ -77,7 +81,12 @@
if (!AllowFreeProfileEditing)
{
var unlockedTitle = UserSetting.UnlockedTitle;
titleSet = titleSet.Where(title => unlockedTitle.Contains((uint)title.TitleId)).ToImmutableHashSet();
titleSet = titleSet.Where(title => unlockedTitle.Contains(title.TitleId)).ToImmutableHashSet();
}
else
{
// Only allow titles in titleUniqueIdList
titleSet = titleSet.Where(title => TitleUniqueIdList.Contains(title.TitleId)).ToImmutableHashSet();
}
titles = titleSet.ToImmutableList().Sort((title, title1) => title.TitleId.CompareTo(title1.TitleId));
var currentTitle = new Title
@ -103,13 +112,15 @@
title.TitleName.Contains(searchString, StringComparison.InvariantCultureIgnoreCase);
}
private void Submit()
private async Task Submit()
{
if (selectedTitle is not null)
{
UserSetting.Title = selectedTitle.TitleName;
UserSetting.TitlePlateId = selectedTitle.TitleRarity;
}
await Js.InvokeVoidAsync("updateTitleText", UserSetting.Title);
MudDialog.Close(DialogResult.Ok(true));
}

View File

@ -0,0 +1,36 @@
@inject HttpClient Client
@inject ISnackbar Snackbar
@inject IJSRuntime Js
<MudDialog>
<DialogContent>
<MudText Typo="Typo.h6">@Otp</MudText>
<div style="height: 1rem"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(_ => CopyToClipboard(Otp))">
@Localizer["Copy to Clipboard"]
</MudButton>
<div style="height: 1rem"></div>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" OnClick="@Submit">@Localizer["Dialog OK"]</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public string Otp { get; set; } = "";
private async Task CopyToClipboard(string text)
{
await Js.InvokeVoidAsync("clipboardCopy.copyText", text);
}
private void Submit()
{
MudDialog.Close(DialogResult.Ok(true));
}
}

View File

@ -1,26 +1,31 @@
namespace TaikoWebUI.Pages.Dialogs;
using System.Net.Http.Headers;
using Blazored.LocalStorage;
namespace TaikoWebUI.Pages.Dialogs;
public partial class ResetPasswordConfirmDialog
{
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public User User { get; set; } = new();
[Inject]
public ILocalStorageService LocalStorage { get; set; } = null!;
[Inject]
public AuthService AuthService { get; set; } = null!;
private void Cancel() => MudDialog.Cancel();
private async Task ResetPassword()
{
var request = new SetPasswordRequest
var request = new ResetPasswordRequest
{
Baid = User.Baid,
Password = "",
Salt = ""
Baid = User.Baid
};
var responseMessage = await Client.PostAsJsonAsync("api/Credentials", request);
var responseMessage = await Client.PostAsJsonAsync("api/Auth/ResetPassword", request);
if (!responseMessage.IsSuccessStatusCode)
{
Snackbar.Add("Something went wrong when resetting password!", Severity.Error);

View File

@ -1,14 +1,21 @@
namespace TaikoWebUI.Pages.Dialogs;
using System.Net.Http.Headers;
using Blazored.LocalStorage;
namespace TaikoWebUI.Pages.Dialogs;
public partial class UserDeleteConfirmDialog
{
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public User User { get; set; } = new();
[Inject]
public ILocalStorageService LocalStorage { get; set; } = null!;
[Inject]
public AuthService AuthService { get; set; } = null!;
private void Cancel() => MudDialog.Cancel();
private async Task DeleteUser()

View File

@ -1,11 +1,9 @@
@inject IGameDataService GameDataService
<MudDialog Class="dialog-user-qr-code">
<MudDialog Class="dialog-user-qr-code">
<DialogContent>
<MudExtensions.MudBarcode Value="@qrCode" BarcodeFormat="ZXing.BarcodeFormat.QR_CODE" Height="300" Width="300" />
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" OnClick="Submit">Ok</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">@Localizer["Dialog OK"]</MudButton>
</DialogActions>
</MudDialog>

View File

@ -1,9 +1,9 @@
@inject IGameDataService GameDataService
@inject HttpClient Client
@inject LoginService LoginService
@inject IJSRuntime JSRuntime
@inject AuthService AuthService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage
@using TaikoWebUI.Utilities;
@page "/Users/{baid:int}/HighScores"
@ -20,9 +20,9 @@
}
else
{
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin)))
@if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{
if (!LoginService.IsLoggedIn)
if (!AuthService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
@ -39,25 +39,104 @@
{
@if (difficulty is not Difficulty.None)
{
<MudTabPanel Text="@ScoreUtils.GetDifficultyTitle(difficulty)"
<MudTabPanel Text="@Localizer[ScoreUtils.GetDifficultyTitle(difficulty)]"
Icon="@ScoreUtils.GetDifficultyIconSvg(difficulty)">
@if (songBestDataMap.TryGetValue(difficulty, out var value))
{
<MudDataGrid Items="@value"
ColumnResizeMode="ResizeMode.None" RowsPerPage="25" Elevation="0">
<Columns>
<TemplateColumn T="SongBestData" Title=@Localizer["Song"] StickyLeft="true">
<CellTemplate>
// Rows per page 25
<MudTable Items="@value" Elevation="0" Striped="true" RowsPerPage="25">
<HeaderContent>
<MudTh>
<MudText Typo="Typo.body2" Style="font-weight:bold">@Localizer["Song Name"]</MudText>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => GameDataService.GetMusicStarLevel(x.SongId, difficulty)">
<MudText>@Localizer["Level"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.Genre">
<MudText>@Localizer["Genre"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.BestScore">
<MudText>@Localizer["Best Score"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.BestCrown">
<MudText>@Localizer["Best Crown"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.BestScoreRank">
<MudText>@Localizer["Best Rank"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.GoodCount">
<MudText>@Localizer["Good"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.OkCount">
<MudText>@Localizer["OK"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.MissCount">
<MudText>@Localizer["Bad"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.DrumrollCount">
<MudText>@Localizer["Drumroll"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.ComboCount">
<MudText>@Localizer["MAX Combo"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.LastPlayTime">
<MudText>@Localizer["Last Played"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.PlayCount">
<MudText>@Localizer["Total Plays"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.ClearCount">
<MudText>@Localizer["Total Clears"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.FullComboCount">
<MudText>@Localizer["Total Full Combos"]</MudText>
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongBestData" SortBy="x => x.PerfectCount">
<MudText>@Localizer["Total Donderful Combos"]</MudText>
</MudTableSortLabel>
</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<div style="width:300px">
<a href="@($"/Users/{Baid}/Songs/{context.Item.SongId}")">
<MudText Typo="Typo.body2" Style="font-weight:bold">@context.Item.MusicName</MudText>
<MudText Typo="Typo.caption">@context.Item.MusicArtist</MudText>
<a href="@($"/Users/{Baid}/Songs/{context.SongId}")">
<MudText Typo="Typo.body2" Style="font-weight:bold">@context.MusicName</MudText>
<MudText Typo="Typo.caption">@context.MusicArtist</MudText>
</a>
</div>
<div>
<MudToggleIconButton Toggled="@context.Item.IsFavorite"
ToggledChanged="@(async () => await OnFavoriteToggled(context.Item))"
<MudToggleIconButton Toggled="@context.IsFavorite"
ToggledChanged="@(async () => await OnFavoriteToggled(context))"
Icon="@Icons.Material.Filled.FavoriteBorder" Color="@Color.Secondary"
ToggledIcon="@Icons.Material.Filled.Favorite" ToggledColor="@Color.Secondary"
Size="Size.Small"
@ -65,54 +144,66 @@
Title="Add to favorites" ToggledTitle="Remove from favorites" />
</div>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn T="SongBestData" Title=@Localizer["Level"] Sortable="false">
<CellTemplate>
</MudTd>
<MudTd>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Star" Size="Size.Small" />
<MudText Typo="Typo.caption" Style="line-height:1;margin-top:2px;margin-right:2px;">@GameDataService.GetMusicStarLevel(@context.Item.SongId, difficulty)</MudText>
<MudText Typo="Typo.caption" Style="line-height:1;margin-top:2px;margin-right:2px;">@GameDataService.GetMusicStarLevel(context.SongId, difficulty)</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn T="SongBestData" Title=@Localizer["Genre"]
Sortable="false" Filterable="true">
<CellTemplate>
<MudChip Style="@ScoreUtils.GetGenreStyle(context.Item.Genre)"
</MudTd>
<MudTd>
<MudChip Style="@ScoreUtils.GetGenreStyle(context.Genre)"
Size="Size.Small">
@ScoreUtils.GetGenreTitle(context.Item.Genre)
@ScoreUtils.GetGenreTitle(context.Genre)
</MudChip>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="data => data.BestScore" Title=@Localizer["Best Score"] />
<TemplateColumn T="SongBestData" Title=@Localizer["Best Crown"]>
<CellTemplate>
<img src="@($"/images/crown_{context.Item.BestCrown}.png")" alt="@(ScoreUtils.GetCrownText(context.Item.BestCrown))" title="@(ScoreUtils.GetCrownText(context.Item.BestCrown))" style="@Constants.ICON_STYLE" />
</CellTemplate>
</TemplateColumn>
<TemplateColumn T="SongBestData" Title=@Localizer["Best Rank"] Sortable="false">
<CellTemplate>
@if (context.Item.BestScoreRank is not ScoreRank.None)
</MudTd>
<MudTd>
<MudText>@context.BestScore</MudText>
</MudTd>
<MudTd>
<img src="@($"/images/crown_{context.BestCrown}.png")" alt="@(ScoreUtils.GetCrownText(context.BestCrown))" title="@(ScoreUtils.GetCrownText(context.BestCrown))" style="@Constants.ICON_STYLE" />
</MudTd>
<MudTd>
@if (context.BestScoreRank is not ScoreRank.None)
{
<img src="@($"/images/rank_{context.Item.BestScoreRank}.png")" alt="@(ScoreUtils.GetRankText(context.Item.BestScoreRank))" title="@(ScoreUtils.GetRankText(context.Item.BestScoreRank))" style="@Constants.ICON_STYLE" />
<img src="@($"/images/rank_{context.BestScoreRank}.png")" alt="@(ScoreUtils.GetRankText(context.BestScoreRank))" title="@(ScoreUtils.GetRankText(context.BestScoreRank))" style="@Constants.ICON_STYLE" />
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="data => data.GoodCount" Title=@Localizer["Good"] Sortable="false" />
<PropertyColumn Property="data => data.OkCount" Title=@Localizer["OK"] Sortable="false" />
<PropertyColumn Property="data => data.MissCount" Title=@Localizer["Bad"] Sortable="false" />
<PropertyColumn Property="data => data.DrumrollCount" Title=@Localizer["Drumroll"] Sortable="false" />
<PropertyColumn Property="data => data.ComboCount" Title=@Localizer["MAX Combo"] Sortable="false" />
<PropertyColumn Property="data => data.LastPlayTime" Title=@Localizer["Last Played"] Hideable="true" />
<PropertyColumn Property="data => data.PlayCount" Title=@Localizer["Total Plays"] Hideable="true" />
<PropertyColumn Property="data => data.ClearCount" Title=@Localizer["Total Clears"] Hideable="true" />
<PropertyColumn Property="data => data.FullComboCount" Title=@Localizer["Total Full Combos"] Hideable="true" />
<PropertyColumn Property="data => data.PerfectCount" Title=@Localizer["Total Donderful Combos"] Hideable="true" />
</Columns>
</MudTd>
<MudTd>
<MudText>@context.GoodCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.OkCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.MissCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.DrumrollCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.ComboCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.LastPlayTime</MudText>
</MudTd>
<MudTd>
<MudText>@context.PlayCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.ClearCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.FullComboCount</MudText>
</MudTd>
<MudTd>
<MudText>@context.PerfectCount</MudText>
</MudTd>
</RowTemplate>
<PagerContent>
<MudDataGridPager T="SongBestData" />
<MudTablePager RowsPerPageString=@Localizer["Rows Per Page:"] />
</PagerContent>
</MudDataGrid>
</MudTable>
}
else
{

View File

@ -2,6 +2,7 @@
using System;
using Microsoft.JSInterop;
namespace TaikoWebUI.Pages;
public partial class HighScores
@ -24,7 +25,7 @@ public partial class HighScores
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
var language = await JSRuntime.InvokeAsync<string>("blazorCulture.get");
var language = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
response.SongBestData.ForEach(data =>
{
@ -43,21 +44,20 @@ public partial class HighScores
.CompareTo(GameDataService.GetMusicIndexBySongId(data2.SongId)));
}
// Set last selected tab from local storage
selectedDifficultyTab = await localStorage.GetItemAsync<int>($"highScoresTab");
selectedDifficultyTab = await LocalStorage.GetItemAsync<int>($"highScoresTab");
// Breadcrumbs
if (LoginService.IsLoggedIn && !LoginService.IsAdmin)
if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{
breadcrumbs.Add(new BreadcrumbItem("Dashboard", href: "/"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
}
else
{
breadcrumbs.Add(new BreadcrumbItem("Users", href: "/Users"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users"));
};
breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem("High Scores", href: $"/Users/{Baid}/HighScores", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(Localizer["High Scores"], href: $"/Users/{Baid}/HighScores", disabled: false));
}
private async Task OnFavoriteToggled(SongBestData data)
@ -78,6 +78,6 @@ public partial class HighScores
private async Task OnTabChanged(int index)
{
selectedDifficultyTab = index;
await localStorage.SetItemAsync($"highScoresTab", selectedDifficultyTab);
await LocalStorage.SetItemAsync($"highScoresTab", selectedDifficultyTab);
}
}

View File

@ -1,12 +1,12 @@
@inject HttpClient Client
@inject IDialogService DialogService
@inject LoginService LoginService
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@page "/Login"
@if (!LoginService.IsLoggedIn)
@if (!AuthService.IsLoggedIn)
{
// Not logged in, show login form
<MudContainer>
@ -21,11 +21,11 @@
<div style="display:flex;flex-direction:column;gap:15px;">
<MudTextField @bind-value="inputAccessCode" InputType="InputType.Text" T="string"
FullWidth="true" Required="@true" RequiredError="Access code is required"
Label="Access Code" Variant="Variant.Outlined" Margin="Margin.Dense" />
Label=@Localizer["Access Code"] Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudTextField @bind-Value="inputPassword" InputType="InputType.Password"
T="string" FullWidth="true" Required="@true"
RequiredError="Password is required"
Label="Password" Variant="Variant.Outlined" Margin="Margin.Dense" />
Label=@Localizer["Password"] Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudButton OnClick="OnLogin" FullWidth="true" StartIcon="@Icons.Material.Filled.Login" Color="Color.Primary" Variant="Variant.Filled">@Localizer["Log In"]</MudButton>
</div>
</MudForm>

View File

@ -5,27 +5,23 @@ public partial class Login
private string inputAccessCode = "";
private MudForm loginForm = default!;
private string inputPassword = "";
private DashboardResponse? response;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
}
private async Task OnLogin()
{
if (response != null)
{
var result = LoginService.Login(inputAccessCode, inputPassword, response);
var options = new DialogOptions() { DisableBackdropClick = true };
var result = await AuthService.Login(inputAccessCode, inputPassword);
var options = new DialogOptions { DisableBackdropClick = true };
switch (result)
{
case 0:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
"Only admin can log in.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
await loginForm.ResetAsync();
break;
case 1:
@ -33,25 +29,30 @@ public partial class Login
break;
case 2:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
"Wrong password!",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
break;
case 3:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
(MarkupString)
"Access code not found.<br />Please play one game with this access code to register it.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
break;
case 4:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
(MarkupString)
"Access code not registered.<br />Please use register button to create a password first.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
break;
case 5:
await DialogService.ShowMessageBox(
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"], null, null, options);
break;
}
}
}
}

View File

@ -0,0 +1,148 @@
@inject IGameDataService GameDataService
@inject HttpClient Client
@inject AuthService AuthService
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@using TaikoWebUI.Utilities;
@using TaikoWebUI.Shared.Models;
@using SharedProject.Enums;
@page "/Users/{baid:int}/PlayHistory"
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-2"></MudBreadcrumbs>
<MudText Typo="Typo.h4">@Localizer["Play History"]</MudText>
<MudGrid Class="my-8">
@if (response is null)
{
<MudContainer Style="display:flex;margin:50px 0;align-items:center;justify-content:center;">
<MudProgressCircular Indeterminate="true" Size="Size.Large" Color="Color.Primary" />
</MudContainer>
}
else
{
@if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{
if (!AuthService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
}
else
{
<MudItem xs="12">
<MudTable Items="@songHistoryDataMap.Values.ToList()" Elevation="0" Filter=@FilterSongs Virtualize="true" RowsPerPage="25" Bordered="false" Dense="true">
<ToolBarContent>
<MudGrid Spacing="2" Justify="Justify.SpaceBetween">
<MudItem xs="12" md="4">
<MudText Typo="Typo.caption">@Localizer["Total Credits Played"]: @songHistoryDataMap.Values.Count</MudText>
</MudItem>
<MudItem xs="12" md="8">
<MudTextField @bind-Value="Search"
Placeholder=@Localizer["Search by Title, Artist or Date"]
Variant="Variant.Outlined"
Clearable="true"
Immediate="true"
Margin="Margin.Dense"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
IconSize="Size.Medium"/>
</MudItem>
</MudGrid>
</ToolBarContent>
<HeaderContent>
<MudTh>
<MudTableSortLabel InitialDirection="SortDirection.Descending" T="List<SongHistoryData>" SortBy="x => x[0].PlayTime">
@Localizer["Play Time"]
</MudTableSortLabel>
</MudTh>
</HeaderContent>
<RowTemplate>
<div class="mt-4 mb-1">
<MudText Typo="Typo.subtitle2">
@CultureInfo.CurrentCulture.TextInfo.ToTitleCase(context[0].PlayTime.ToString(Localizer["DateFormat"]))
</MudText>
</div>
</RowTemplate>
<ChildRowContent>
<MudTable Items="@context" T="SongHistoryData" Context="songHistoryData" Elevation="0" Striped="false" Hover="false" ReadOnly="true" Outlined="true" Class="mb-8">
<HeaderContent>
<MudTh>@Localizer["Difficulty"]</MudTh>
<MudTh>@Localizer["Level"]</MudTh>
<MudTh Style="width:500px">@Localizer["Song Name"]</MudTh>
<MudTh>@Localizer["Genre"]</MudTh>
<MudTh>@Localizer["Score"]</MudTh>
<MudTh>@Localizer["Crown"]</MudTh>
<MudTh>@Localizer["Rank"]</MudTh>
<MudTh>@Localizer["Good"]</MudTh>
<MudTh>@Localizer["OK"]</MudTh>
<MudTh>@Localizer["Bad"]</MudTh>
<MudTh>@Localizer["Drumroll"]</MudTh>
<MudTh>@Localizer["MAX Combo"]</MudTh>
</HeaderContent>
<RowTemplate>
@* Difficulty rating *@
<MudTd DataLabel="Difficulty" Style="text-align:center"><MudIcon Style=@IconStyle Icon="@GetDifficultyIcon(songHistoryData.Difficulty)" /></MudTd>
@* Star rating *@
<MudTd DataLabel="Stars" Style="text-align:center">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Star" Size="Size.Small" />
<MudText Typo="Typo.caption" Style="line-height:1;margin-top:2px;margin-right:2px;">@songHistoryData.Stars</MudText>
</MudStack>
</MudTd>
@* Song title *@
<MudTd DataLabel="Song">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<div>
<a href="@($"/Users/{Baid}/Songs/{songHistoryData.SongId}")">
<MudText Typo="Typo.body2" Style="font-weight:bold">@songHistoryData.MusicName</MudText>
<MudText Typo="Typo.caption">@songHistoryData.MusicArtist</MudText>
</a>
</div>
<div>
<MudToggleIconButton Toggled="@songHistoryData.IsFavorite"
ToggledChanged="@(async () => await OnFavoriteToggled(songHistoryData))"
Icon="@Icons.Material.Filled.FavoriteBorder" Color="@Color.Secondary"
ToggledIcon="@Icons.Material.Filled.Favorite" ToggledColor="@Color.Secondary"
Size="Size.Small"
ToggledSize="Size.Small"
Title="Add to favorites" ToggledTitle="Remove from favorites" />
</div>
</MudStack>
</MudTd>
@* Genre display *@
<MudTd DataLabel="Genre" Style="text-align:left">
<MudChip Style=@GetGenreStyle(songHistoryData.Genre) Size="Size.Small">@GetGenreTitle(songHistoryData.Genre)</MudChip>
</MudTd>
<MudTd>@(songHistoryData.Score)</MudTd>
<MudTd Style="text-align:center"><img src="@($"/images/crown_{songHistoryData.Crown}.png")" alt="@(GetCrownText(songHistoryData.Crown))" title="@(GetCrownText(songHistoryData.Crown))" style=@IconStyle /></MudTd>
<MudTd Style="text-align:center">
@if (songHistoryData.ScoreRank is not ScoreRank.None)
{
<img src="@($"/images/rank_{songHistoryData.ScoreRank}.png")" alt="@(GetRankText(songHistoryData.ScoreRank))" title="@(GetRankText(songHistoryData.ScoreRank))" style=@IconStyle />
}
</MudTd>
<MudTd>@(songHistoryData.GoodCount)</MudTd>
<MudTd>@(songHistoryData.OkCount)</MudTd>
<MudTd>@(songHistoryData.MissCount)</MudTd>
<MudTd>@(songHistoryData.DrumrollCount)</MudTd>
<MudTd>@(songHistoryData.ComboCount)</MudTd>
</RowTemplate>
</MudTable>
</ChildRowContent>
<PagerContent>
<MudTablePager RowsPerPageString=@Localizer["Rows Per Page:"] />
</PagerContent>
</MudTable>
</MudItem>
}
}
</MudGrid>

View File

@ -0,0 +1,177 @@
using static MudBlazor.Colors;
using System;
using static MudBlazor.CategoryTypes;
using System.Globalization;
using Microsoft.JSInterop;
using TaikoWebUI.Shared.Models;
namespace TaikoWebUI.Pages;
public partial class PlayHistory
{
[Parameter]
public int Baid { get; set; }
private const string IconStyle = "width:25px; height:25px;";
private string Search { get; set; } = string.Empty;
private string? currentLanguage;
private SongHistoryResponse? response;
private Dictionary<DateTime, List<SongHistoryData>> songHistoryDataMap = new();
private readonly List<BreadcrumbItem> breadcrumbs = new();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<SongHistoryResponse>($"api/PlayHistory/{(uint)Baid}");
response.ThrowIfNull();
currentLanguage = await JSRuntime.InvokeAsync<string>("blazorCulture.get");
response.SongHistoryData.ForEach(data =>
{
var songId = data.SongId;
data.Genre = GameDataService.GetMusicGenreBySongId(songId);
data.MusicName = GameDataService.GetMusicNameBySongId(songId, string.IsNullOrEmpty(currentLanguage) ? "ja" : currentLanguage);
data.MusicArtist = GameDataService.GetMusicArtistBySongId(songId, string.IsNullOrEmpty(currentLanguage) ? "ja" : currentLanguage);
data.Stars = GameDataService.GetMusicStarLevel(songId, data.Difficulty);
data.ShowDetails = false;
});
songHistoryDataMap = response.SongHistoryData.GroupBy(data => data.PlayTime).ToDictionary(data => data.Key,data => data.ToList());
foreach (var songHistoryDataList in songHistoryDataMap.Values)
{
songHistoryDataList.Sort((data1, data2) => data1.SongNumber.CompareTo(data2.SongNumber));
}
}
private static string GetCrownText(CrownType crown)
{
return crown switch
{
CrownType.None => "Fail",
CrownType.Clear => "Clear",
CrownType.Gold => "Full Combo",
CrownType.Dondaful => "Donderful Combo",
_ => ""
};
}
private static string GetRankText(ScoreRank rank)
{
return rank switch
{
ScoreRank.White => "Stylish",
ScoreRank.Bronze => "Stylish",
ScoreRank.Silver => "Stylish",
ScoreRank.Gold => "Graceful",
ScoreRank.Sakura => "Graceful",
ScoreRank.Purple => "Graceful",
ScoreRank.Dondaful => "Top Class",
_ => ""
};
}
private static string GetDifficultyTitle(Difficulty difficulty)
{
return difficulty switch
{
Difficulty.Easy => "Easy",
Difficulty.Normal => "Normal",
Difficulty.Hard => "Hard",
Difficulty.Oni => "Oni",
Difficulty.UraOni => "Ura Oni",
_ => ""
};
}
private static string GetDifficultyIcon(Difficulty difficulty)
{
return $"<image href='/images/difficulty_{difficulty}.png' alt='{difficulty}' width='24' height='24'/>";
}
private static string GetGenreTitle(SongGenre genre)
{
return genre switch
{
SongGenre.Pop => "Pop",
SongGenre.Anime => "Anime",
SongGenre.Kids => "Kids",
SongGenre.Vocaloid => "Vocaloid",
SongGenre.GameMusic => "Game Music",
SongGenre.NamcoOriginal => "NAMCO Original",
SongGenre.Variety => "Variety",
SongGenre.Classical => "Classical",
_ => ""
};
}
private static string GetGenreStyle(SongGenre genre)
{
return genre switch
{
SongGenre.Pop => "background: #42c0d2; color: #fff",
SongGenre.Anime => "background: #ff90d3; color: #fff",
SongGenre.Kids => "background: #fec000; color: #fff",
SongGenre.Vocaloid => "background: #ddd; color: #000",
SongGenre.GameMusic => "background: #cc8aea; color: #fff",
SongGenre.NamcoOriginal => "background: #ff7027; color: #fff",
SongGenre.Variety => "background: #1dc83b; color: #fff",
SongGenre.Classical => "background: #bfa356; color: #000",
_ => ""
};
}
private bool FilterSongs(List<SongHistoryData> songHistoryDataList)
{
if (string.IsNullOrWhiteSpace(Search))
{
return true;
}
var language = currentLanguage ?? "ja";
if (songHistoryDataList[0].PlayTime
.ToString("dddd d MMMM yyyy - HH:mm", CultureInfo.CreateSpecificCulture(language))
.Contains(Search, StringComparison.OrdinalIgnoreCase))
{
return true;
}
foreach (var songHistoryData in songHistoryDataList)
{
if (songHistoryData.MusicName.Contains(Search, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (songHistoryData.MusicArtist.Contains(Search, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private async Task OnFavoriteToggled(SongHistoryData data)
{
var request = new SetFavoriteRequest
{
Baid = (uint)Baid,
IsFavorite = !data.IsFavorite,
SongId = data.SongId
};
var result = await Client.PostAsJsonAsync("api/FavoriteSongs", request);
if (result.IsSuccessStatusCode)
{
data.IsFavorite = !data.IsFavorite;
}
}
}

View File

@ -2,15 +2,15 @@
@inject HttpClient Client
@inject IGameDataService GameDataService
@inject IDialogService DialogService
@inject LoginService LoginService
@inject IJSRuntime Js
@inject AuthService AuthService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
@if (response is not null)
{
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin)))
@if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{
if (!LoginService.IsLoggedIn)
if (!AuthService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
@ -47,7 +47,7 @@
<MudGrid>
<MudItem xs="12" md="8">
@if (LoginService.AllowFreeProfileEditing)
@if (AuthService.AllowFreeProfileEditing)
{
<MudTextField TextChanged="UpdateTitle" @bind-Value="@response.Title" Label=@Localizer["Title"]/>
}
@ -55,18 +55,17 @@
{
<MudTextField ReadOnly="true" @bind-Value="@response.Title" Label=@Localizer["Title"]/>
}
<MudButton Color="Color.Primary" Class="mt-1" Size="Size.Small" OnClick="@((_) => OpenChooseTitleDialog())">
<MudButton Color="Color.Primary" Class="mt-1" Size="Size.Small" OnClick="@(_ => OpenChooseTitleDialog())">
@Localizer["Select a Title"]
</MudButton>
</MudItem>
@if (LoginService.AllowFreeProfileEditing)
@if (AuthService.AllowFreeProfileEditing)
{
<MudItem xs="12" md="4">
<MudSelect @bind-Value="@response.TitlePlateId" Label=@Localizer["Title Plate"]>
@for (uint i = 0; i < TitlePlateStrings.Length; i++)
@foreach (var index in titlePlateIdList)
{
var index = i;
<MudSelectItem Value="@i">@TitlePlateStrings[index]</MudSelectItem>
<MudSelectItem Value="@index">@TitlePlateStrings[index]</MudSelectItem>
}
</MudSelect>
</MudItem>
@ -124,100 +123,41 @@
<MudGrid>
<MudItem xs="12">
<MudStack Spacing="4" Class="mb-8">
@if (LoginService.AllowFreeProfileEditing)
{
<MudSelect @bind-Value="@response.Head" Label=@Localizer["Head"]>
@for (var i = 0; i < costumeFlagArraySizes[1]; i++)
@foreach (var index in headUniqueIdList)
{
var index = (uint)i;
var costumeTitle = GameDataService.GetHeadTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Body" Label=@Localizer["Body"]>
@for (var i = 0; i < costumeFlagArraySizes[2]; i++)
@foreach (var index in bodyUniqueIdList)
{
var index = (uint)i;
var costumeTitle = GameDataService.GetBodyTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Face" Label=@Localizer["Face"]>
@for (var i = 0; i < costumeFlagArraySizes[3]; i++)
@foreach (var index in faceUniqueIdList)
{
var index = (uint)i;
var costumeTitle = GameDataService.GetFaceTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Kigurumi" Label=@Localizer["Kigurumi"]>
@for (var i = 0; i < costumeFlagArraySizes[0]; i++)
@foreach (var index in kigurumiUniqueIdList)
{
var index = (uint)i;
var costumeTitle = GameDataService.GetKigurumiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Puchi" Label=@Localizer["Puchi"]>
@for (var i = 0; i < costumeFlagArraySizes[4]; i++)
@foreach (var index in puchiUniqueIdList)
{
var index = (uint)i;
var costumeTitle = GameDataService.GetPuchiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
}
else
{
<MudSelect @bind-Value="@response.Head" Label=@Localizer["Head"]>
@foreach (var i in unlockedHeadCostumes)
{
var index = i;
var costumeTitle = GameDataService.GetHeadTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Body" Label=@Localizer["Body"]>
@foreach (var i in unlockedBodyCostumes)
{
var index = i;
var costumeTitle = GameDataService.GetBodyTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Face" Label=@Localizer["Face"]>
@foreach (var i in unlockedFaceCostumes)
{
var index = i;
var costumeTitle = GameDataService.GetFaceTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Kigurumi" Label=@Localizer["Kigurumi"]>
@foreach (var i in unlockedKigurumiCostumes)
{
var index = i;
var costumeTitle = GameDataService.GetKigurumiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Puchi" Label=@Localizer["Puchi"]>
@foreach (var i in unlockedPuchiCostumes)
{
var index = i;
var costumeTitle = GameDataService.GetPuchiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
}
</MudStack>
<MudStack Row="true">
@ -311,7 +251,7 @@
<MudStack Spacing="4" Class="sticky" Style="top:100px">
<MudPaper Elevation="0" Outlined="true">
<MudTabs Rounded="true" Border="true" PanelClass="pa-8">
<MudTabPanel Text="@Localizer["Player"]">
<MudTabPanel Text=@Localizer["Player"]>
<MudItem style="height: auto">
@* Player Visualizer *@
<MudItem style="text-align: center;">
@ -349,12 +289,13 @@
<MudItem Style="position:absolute; top: 47%; left:0; right:1.1%; height:41%; width:min(96%, 320px); margin: 0 auto; z-index:2">
@* Name textoffset to the right for Dan Rank *@
<MudItem Style=@($"position:absolute; height:100%; right: 10%; {(response.IsDisplayDanOnNamePlate ? "width:46%;" : "width:80%;")}")>
<MudText Id="nameplate-name-outline" Style="position:absolute; height:100%; top: 0; left: 0; right: 0; margin: auto auto; font-family: 'Nijiiro', sans-serif; -webkit-text-stroke: 5px; -webkit-text-stroke-color: black">@response.MyDonName</MudText>
<MudText Id="nameplate-name-outline" Style="position:absolute; height:100%; top: 0; left: 0; right: 0; margin: auto auto; font-family: 'Nijiiro', sans-serif; -webkit-text-stroke-width: 5px; -webkit-text-stroke-color: black">@response.MyDonName</MudText>
<MudText Id="nameplate-name" Style="position:absolute; height:100%; top: 0; left: 0; right: 0; margin: auto auto; font-family: 'Nijiiro', sans-serif; color:white">@response.MyDonName</MudText>
</MudItem>
</MudItem>
<MudImage onload="nameplateLoaded()" Id="nameplate" Fluid="true" Style="position: relative; top: 0; left: 0;" Src="images/Nameplates/nameplate.png" />
<MudImage Fluid="true" Style="position:absolute; top: 0; left: 0; right: 0; margin: 0 auto;" Src=@($"images/Nameplates/nameplate_{TitlePlateStrings[response.TitlePlateId].Replace(' ', '_')}.png") />
<MudImage onload="nameplateLoaded()" Id="nameplate" Fluid="true" Style="position: relative; bottom: 0; left: 0;" Src="images/Nameplates/nameplate.png" />
@* Check if image does not exist, use nameplate_Wood.png *@
<MudImage Fluid="true" Style="position:absolute; bottom: 0%; left: 0; right: 0; margin: 0 auto;" onerror="this.src='images/Nameplates/nameplate_Wood.png'" Src=@($"images/Nameplates/nameplate_{TitlePlateStrings[response.TitlePlateId].Replace(' ', '_')}.png") />
@if (response.IsDisplayDanOnNamePlate)
{
<MudImage Fluid="true" Style="position:absolute; top: 0; left: 0; right: 0; margin: 0 auto;" Src="images/Nameplates/nameplate_dan.png" />
@ -460,17 +401,17 @@ else
@code {
private async Task UpdateMyDonName()
{
@if (response is not null) await Js.InvokeVoidAsync("updateMyDonNameText", response.MyDonName);
@if (response is not null) await JsRuntime.InvokeVoidAsync("updateMyDonNameText", response.MyDonName);
}
private async Task UpdateTitle()
{
@if (response is not null) await Js.InvokeVoidAsync("updateTitleText", response.Title);
@if (response is not null) await JsRuntime.InvokeVoidAsync("updateTitleText", response.Title);
}
private async Task UpdateScoreboard(Difficulty difficulty)
{
UpdateScores(difficulty);
await Js.InvokeVoidAsync("updateScoreboardText", scoresArray);
await JsRuntime.InvokeVoidAsync("updateScoreboardText", scoresArray);
}
}

View File

@ -135,17 +135,17 @@ public partial class Profile
"Synth Drum", "Shuriken", "Bubble Pop", "Electric Guitar"
};
private static readonly string[] LanguageStrings =
{
"Japanese", "English", "Chinese (Traditional)", "Korean", "Chinese (Simplified)"
};
private static readonly string[] TitlePlateStrings =
{
"Wood", "Rainbow", "Gold", "Purple",
"AI 1", "AI 2", "AI 3", "AI 4"
};
private static readonly string[] LanguageStrings =
{
"Japanese", "English", "Chinese (Traditional)", "Korean", "Chinese (Simplified)"
};
private static readonly string[] DifficultySettingCourseStrings =
{
"None", "Set up each time",
@ -170,13 +170,13 @@ public partial class Profile
private Difficulty highestDifficulty = Difficulty.Easy;
private List<int> costumeFlagArraySizes = new();
private List<uint> unlockedHeadCostumes = new();
private List<uint> unlockedBodyCostumes = new();
private List<uint> unlockedFaceCostumes = new();
private List<uint> unlockedKigurumiCostumes = new();
private List<uint> unlockedPuchiCostumes = new();
private List<uint> kigurumiUniqueIdList = new();
private List<uint> headUniqueIdList = new();
private List<uint> bodyUniqueIdList = new();
private List<uint> faceUniqueIdList = new();
private List<uint> puchiUniqueIdList = new();
private List<uint> titleUniqueIdList = new();
private List<uint> titlePlateIdList = new();
private int[] scoresArray = new int[10];
@ -187,27 +187,19 @@ public partial class Profile
response = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
response.ThrowIfNull();
if (LoginService.IsLoggedIn && !LoginService.IsAdmin)
if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{
breadcrumbs.Add(new BreadcrumbItem("Dashboard", href: "/"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
}
else
{
breadcrumbs.Add(new BreadcrumbItem("Users", href: "/Users"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users"));
};
breadcrumbs.Add(new BreadcrumbItem($"{response.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem("Profile", href: $"/Users/{Baid}/Profile", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Profile"], href: $"/Users/{Baid}/Profile", disabled: false));
if (response != null)
{
unlockedHeadCostumes = response.UnlockedHead.Distinct().OrderBy(x => x).ToList();
unlockedBodyCostumes = response.UnlockedBody.Distinct().OrderBy(x => x).ToList();
unlockedFaceCostumes = response.UnlockedFace.Distinct().OrderBy(x => x).ToList();
unlockedKigurumiCostumes = response.UnlockedKigurumi.Distinct().OrderBy(x => x).ToList();
unlockedPuchiCostumes = response.UnlockedPuchi.Distinct().OrderBy(x => x).ToList();
}
costumeFlagArraySizes = GameDataService.GetCostumeFlagArraySizes();
InitializeAvailableCostumes();
InitializeAvailableTitles();
songresponse = await Client.GetFromJsonAsync<SongBestResponse>($"api/PlayData/{Baid}");
songresponse.ThrowIfNull();
@ -229,14 +221,87 @@ public partial class Profile
.CompareTo(GameDataService.GetMusicIndexBySongId(data2.SongId)));
}
for (int i = 0; i < (int)Difficulty.UraOni; i++)
for (var i = 0; i < (int)Difficulty.UraOni; i++)
if (songBestDataMap.TryGetValue((Difficulty)i, out var values))
{
highestDifficulty = (Difficulty)i;
}
if (response != null) UpdateScores(response.AchievementDisplayDifficulty);
}
UpdateScores(response.AchievementDisplayDifficulty);
private void InitializeAvailableCostumes()
{
var unlockedKigurumi = response != null ? response.UnlockedKigurumi : new List<uint>();
var unlockedHead = response != null ? response.UnlockedHead : new List<uint>();
var unlockedBody = response != null ? response.UnlockedBody : new List<uint>();
var unlockedFace = response != null ? response.UnlockedFace : new List<uint>();
var unlockedPuchi = response != null ? response.UnlockedPuchi : new List<uint>();
if (AuthService.AllowFreeProfileEditing)
{
kigurumiUniqueIdList = GameDataService.GetKigurumiUniqueIdList();
headUniqueIdList = GameDataService.GetHeadUniqueIdList();
bodyUniqueIdList = GameDataService.GetBodyUniqueIdList();
faceUniqueIdList = GameDataService.GetFaceUniqueIdList();
puchiUniqueIdList = GameDataService.GetPuchiUniqueIdList();
// Lock costumes in LockedCostumesList but not in UnlockedCostumesList
var lockedKigurumiUniqueIdList = GameDataService.GetLockedKigurumiUniqueIdList().Except(unlockedKigurumi).ToList();
var lockedHeadUniqueIdList = GameDataService.GetLockedHeadUniqueIdList().Except(unlockedHead).ToList();
var lockedBodyUniqueIdList = GameDataService.GetLockedBodyUniqueIdList().Except(unlockedBody).ToList();
var lockedFaceUniqueIdList = GameDataService.GetLockedFaceUniqueIdList().Except(unlockedFace).ToList();
var lockedPuchiUniqueIdList = GameDataService.GetLockedPuchiUniqueIdList().Except(unlockedPuchi).ToList();
lockedKigurumiUniqueIdList.ForEach(id => kigurumiUniqueIdList.Remove(id));
lockedHeadUniqueIdList.ForEach(id => headUniqueIdList.Remove(id));
lockedBodyUniqueIdList.ForEach(id => bodyUniqueIdList.Remove(id));
lockedFaceUniqueIdList.ForEach(id => faceUniqueIdList.Remove(id));
lockedPuchiUniqueIdList.ForEach(id => puchiUniqueIdList.Remove(id));
}
else
{
// Only unlock costumes that are in both UnlockedCostumesList and CostumeList
kigurumiUniqueIdList = GameDataService.GetKigurumiUniqueIdList().Intersect(unlockedKigurumi).ToList();
headUniqueIdList = GameDataService.GetHeadUniqueIdList().Intersect(unlockedHead).ToList();
bodyUniqueIdList = GameDataService.GetBodyUniqueIdList().Intersect(unlockedBody).ToList();
faceUniqueIdList = GameDataService.GetFaceUniqueIdList().Intersect(unlockedFace).ToList();
puchiUniqueIdList = GameDataService.GetPuchiUniqueIdList().Intersect(unlockedPuchi).ToList();
}
}
private void InitializeAvailableTitlePlates()
{
titlePlateIdList = GameDataService.GetTitlePlateIdList().ToList();
// Cut off ids longer than TitlePlateStrings
titlePlateIdList = titlePlateIdList.Where(id => id < TitlePlateStrings.Length).Except(GameDataService.GetLockedTitlePlateIdList()).ToList();
}
private void InitializeAvailableTitles()
{
InitializeAvailableTitlePlates();
var unlockedTitle = response != null ? response.UnlockedTitle : new List<uint>();
if (AuthService.AllowFreeProfileEditing)
{
titleUniqueIdList = GameDataService.GetTitleUniqueIdList();
var titles = GameDataService.GetTitles();
// Lock titles in LockedTitlesList but not in UnlockedTitle
var lockedTitleUniqueIdList = GameDataService.GetLockedTitleUniqueIdList().ToList();
var lockedTitlePlateIdList = GameDataService.GetLockedTitlePlateIdList().ToList();
// Unlock titles in UnlockedTitlesList
lockedTitleUniqueIdList = lockedTitleUniqueIdList.Except(unlockedTitle).ToList();
// Find uniqueIds of titles with rarity in lockedTitlePlateIdList
lockedTitleUniqueIdList.AddRange(titles.Where(title => lockedTitlePlateIdList.Contains(title.TitleRarity)).Select(title => title.TitleId));
titleUniqueIdList = titleUniqueIdList.Except(lockedTitleUniqueIdList).ToList();
}
else
{
// Only unlock titles that are in both UnlockedTitlesList and TitleList
titleUniqueIdList = GameDataService.GetTitleUniqueIdList().Intersect(unlockedTitle).ToList();
}
}
private async Task SaveOptions()
@ -246,7 +311,7 @@ public partial class Profile
isSavingOptions = false;
// Adjust breadcrumb if name is changed
if (response != null && response.MyDonName != null)
if (response != null)
{
breadcrumbs[^2] = new BreadcrumbItem($"{response.MyDonName}", href: null, disabled: true);
}
@ -260,8 +325,8 @@ public partial class Profile
if (difficulty is Difficulty.None) difficulty = highestDifficulty;
if (songBestDataMap.TryGetValue(difficulty, out var values))
{
if (!songBestDataMap.TryGetValue(difficulty, out var values)) return;
foreach (var value in values)
{
switch (value.BestScoreRank)
@ -302,8 +367,6 @@ public partial class Profile
break;
}
}
}
}
public static string CostumeOrDefault(string file, uint id, string defaultfile)
{
@ -327,7 +390,8 @@ public partial class Profile
var parameters = new DialogParameters<ChooseTitleDialog>
{
{x => x.UserSetting, response},
{x => x.AllowFreeProfileEditing, LoginService.AllowFreeProfileEditing}
{x => x.AllowFreeProfileEditing, AuthService.AllowFreeProfileEditing},
{x => x.TitleUniqueIdList, titleUniqueIdList}
};
var dialog = DialogService.Show<ChooseTitleDialog>("Player Titles", parameters, options);
var result = await dialog.Result;

View File

@ -1,16 +1,16 @@
@inject HttpClient Client
@inject IDialogService DialogService
@inject LoginService LoginService
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@page "/Register"
@if (LoginService.OnlyAdmin || !LoginService.LoginRequired)
@if (AuthService.OnlyAdmin || !AuthService.LoginRequired)
{
Console.WriteLine("Registration is disabled. Redirecting to Dashboard...");
NavigationManager.NavigateTo("/");
}
else if (LoginService.IsLoggedIn)
else if (AuthService.IsLoggedIn)
{
// User is already logged in. Redirect to dashboard.
NavigationManager.NavigateTo("/");
@ -22,30 +22,32 @@ else
<MudItem xs="12" md="6" lg="4">
<MudCard Elevation="0" Outlined="true">
<MudCardHeader>
<MudText Typo="Typo.h5">Register</MudText>
<MudText Typo="Typo.h5">@Localizer["Register"]</MudText>
</MudCardHeader>
<MudCardContent>
<MudForm @ref="registerForm">
<div style="display:flex;flex-direction:column;gap:15px;">
<MudTextField @bind-value="accessCode" InputType="InputType.Text" T="string"
FullWidth="true" Required="@true" RequiredError="Access Code is required"
Label="Access Code" Variant="Variant.Outlined" Margin="Margin.Dense" />
@if (LoginService.RegisterWithLastPlayTime)
FullWidth="true" Required="@true" RequiredError=@Localizer["Access Code is required"]
Label=@Localizer["Access Code"] Variant="Variant.Outlined" Margin="Margin.Dense" />
@if (AuthService.RegisterWithLastPlayTime)
{
<MudDatePicker @ref="datePicker" Label="Last Play Date" @bind-Date="date" AutoClose="true" Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudTimePicker @ref="timePicker" AmPm="true" Label="Last Play Time(5 min around credit end)" @bind-Time="time" AutoClose="true" Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudTextField @bind-value="inviteCode" InputType="InputType.Text" T="string"
FullWidth="true" Label=@Localizer["Invite Code (Optional)"]/>
<MudDatePicker @ref="datePicker" Label=@Localizer["Last Play Date"] @bind-Date="date" AutoClose="true" Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudTimePicker @ref="timePicker" AmPm="true" Label=@Localizer["Last Play Time(5 Min Around Credit End)"] @bind-Time="time" AutoClose="true" Variant="Variant.Outlined" Margin="Margin.Dense" />
}
<MudTextField @bind-Value="password" InputType="InputType.Password"
T="string" FullWidth="true" Required="@true"
RequiredError="Password is required"
Label="Password" Variant="Variant.Outlined" Margin="Margin.Dense">
RequiredError=@Localizer["Password is Required"]
Label=@Localizer["Password"] Variant="Variant.Outlined" Margin="Margin.Dense">
</MudTextField>
<MudTextField @bind-Value="confirmPassword" InputType="InputType.Password"
T="string" FullWidth="true" Required="@true"
RequiredError="Confirm password is required"
Label="Confirm Password" Variant="Variant.Outlined" Margin="Margin.Dense">
RequiredError=@Localizer["Confirm Password is Required"]
Label=@Localizer["Confirm Password"] Variant="Variant.Outlined" Margin="Margin.Dense">
</MudTextField>
<MudButton OnClick="OnRegister" FullWidth="true" StartIcon="@Icons.Material.Filled.AddCard" Color="Color.Primary" Variant="Variant.Filled">Register</MudButton>
<MudButton OnClick="OnRegister" FullWidth="true" StartIcon="@Icons.Material.Filled.AddCard" Color="Color.Primary" Variant="Variant.Filled">@Localizer["Register"]</MudButton>
</div>
</MudForm>
</MudCardContent>

View File

@ -11,67 +11,68 @@ public partial class Register
private MudTimePicker timePicker = new();
private DateTime? date = DateTime.Today;
private TimeSpan? time = new TimeSpan(00, 45, 00);
private DashboardResponse? response;
private string inviteCode = "";
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
}
private async Task OnRegister()
{
var inputDateTime = date!.Value.Date + time!.Value;
if (response != null)
{
var result = await LoginService.Register(accessCode, inputDateTime, password, confirmPassword, response, Client);
var options = new DialogOptions() { DisableBackdropClick = true };
var result = await AuthService.Register(accessCode, inputDateTime, password, confirmPassword, inviteCode);
var options = new DialogOptions { DisableBackdropClick = true };
switch (result)
{
case 0:
await DialogService.ShowMessageBox(
"Error",
"Only admin can log in.",
"Ok", null, null, options);
Localizer["Error"],
"Only admin can register.",
Localizer["Dialog OK"], null, null, options);
NavigationManager.NavigateTo("/");
break;
case 1:
await DialogService.ShowMessageBox(
"Success",
Localizer["Success"],
"Access code registered successfully.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
NavigationManager.NavigateTo("/Login");
break;
case 2:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
"Confirm password is not the same as password.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
break;
case 3:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
(MarkupString)
"Access code not found.<br />Please play one game with this access code to register it.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
break;
case 4:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
(MarkupString)
"Access code is already registered, please use set password to login.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
NavigationManager.NavigateTo("/Login");
break;
case 5:
await DialogService.ShowMessageBox(
"Error",
Localizer["Error"],
(MarkupString)
"Wrong last play time.<br />If you have forgotten when you last played, please play another game with this access code.",
"Ok", null, null, options);
Localizer["Dialog OK"], null, null, options);
break;
case 6:
await DialogService.ShowMessageBox(
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"], null, null, options);
break;
}
}
}
}

View File

@ -2,34 +2,26 @@
@inject IGameDataService GameDataService
@inject HttpClient Client
@inject LoginService LoginService
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@inject IJSRuntime JsRuntime
@using TaikoWebUI.Utilities;
@using TaikoWebUI.Components.Song;
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin)))
@if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{
if (!LoginService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
NavigationManager.NavigateTo(AuthService.IsLoggedIn ? "/" : "/Login");
}
else
{
if (response is not null)
{
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-3"></MudBreadcrumbs>
<MudText Typo="Typo.h5">@SongTitle</MudText>
<MudText Typo="Typo.body2">@SongArtist</MudText>
<MudText Typo="Typo.h5">@songTitle</MudText>
<MudText Typo="Typo.body2">@songArtist</MudText>
<MudGrid Class="my-4 pb-10">
<MudItem xs="12">
<PlayHistoryCard Items="@SongBestData?.RecentPlayData" />
<PlayHistoryCard Items="@songHistoryData" />
</MudItem>
</MudGrid>
}

View File

@ -11,47 +11,48 @@ namespace TaikoWebUI.Pages
public int Baid { get; set; }
private UserSetting? userSetting;
private SongBestResponse? response;
private SongBestData? SongBestData;
private List<BreadcrumbItem> breadcrumbs = new List<BreadcrumbItem>();
private SongHistoryResponse? response;
private List<SongHistoryData>? songHistoryData;
private readonly List<BreadcrumbItem> breadcrumbs = new();
private string SongTitle = string.Empty;
private string SongArtist = string.Empty;
private string songTitle = string.Empty;
private string songArtist = string.Empty;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<SongBestResponse>($"api/PlayData/{Baid}");
response = await Client.GetFromJsonAsync<SongHistoryResponse>($"api/PlayHistory/{(uint)Baid}");
response.ThrowIfNull();
SongBestData = response.SongBestData.FirstOrDefault(x => x.SongId == SongId);
// Get all song best data with SongId
songHistoryData = response.SongHistoryData.Where(data => data.SongId == (uint)SongId).ToList();
// Get user settings
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
// Get song title and artist
var language = await JSRuntime.InvokeAsync<string>("blazorCulture.get");
SongTitle = GameDataService.GetMusicNameBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language);
SongArtist = GameDataService.GetMusicArtistBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language);
var language = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
songTitle = GameDataService.GetMusicNameBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language);
songArtist = GameDataService.GetMusicArtistBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language);
// Breadcrumbs
var _songTitle = SongTitle;
if (_songTitle.Length > 20)
var formattedSongTitle = songTitle;
if (formattedSongTitle.Length > 20)
{
_songTitle = _songTitle.Substring(0, 20) + "...";
formattedSongTitle = string.Concat(formattedSongTitle.AsSpan(0, 20), "...");
}
if (LoginService.IsLoggedIn && !LoginService.IsAdmin)
if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{
breadcrumbs.Add(new BreadcrumbItem("Dashboard", href: "/"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
}
else
{
breadcrumbs.Add(new BreadcrumbItem("Users", href: "/Users"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users"));
};
breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem("Songs", href: $"/Users/{Baid}/Songs", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(_songTitle, href: $"/Users/{Baid}/Songs/{SongId}", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(formattedSongTitle, href: $"/Users/{Baid}/Songs/{SongId}", disabled: false));
}
}
}

View File

@ -1,16 +1,15 @@
@inject IGameDataService GameDataService
@inject HttpClient Client
@inject LoginService LoginService
@inject IJSRuntime JSRuntime
@inject AuthService AuthService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
@using TaikoWebUI.Utilities;
@using TaikoWebUI.Shared.Models;
@using SharedProject.Enums;
@page "/Users/{baid:int}/Songs"
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-2"></MudBreadcrumbs>
<MudText Typo="Typo.h4">@Localizer["Key_01"]</MudText>
<MudText Typo="Typo.h4">@Localizer["Song List"]</MudText>
<MudGrid Class="my-8">
@if (response is null)
@ -21,16 +20,9 @@
}
else
{
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin)))
@if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{
if (!LoginService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
NavigationManager.NavigateTo(AuthService.IsLoggedIn ? "/" : "/Login");
}
else
{
@ -65,12 +57,12 @@
<HeaderContent>
<MudTh>
<MudTableSortLabel T="MusicDetail" SortBy="context => GameDataService.GetMusicNameBySongId(context.SongId, CurrentLanguage)">
Song Title / Artist
@Localizer["Song Title / Artist"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="MusicDetail" SortBy="context => context.Genre">
Genre
@Localizer["Genre"]
</MudTableSortLabel>
</MudTh>
@foreach (var difficulty in Enum.GetValues<Difficulty>())
@ -138,7 +130,7 @@
}
</RowTemplate>
<PagerContent>
<MudTablePager />
<MudTablePager RowsPerPageString=@Localizer["Rows Per Page"] />
</PagerContent>
</MudTable>
</MudItem>

View File

@ -1,4 +1,5 @@
using Microsoft.JSInterop;
using System.Reflection.Emit;
using Microsoft.JSInterop;
using TaikoWebUI.Shared.Models;
@ -9,8 +10,6 @@ public partial class SongList
[Parameter]
public int Baid { get; set; }
private const string IconStyle = "width:25px; height:25px;";
private string Search { get; set; } = string.Empty;
private string GenreFilter { get; set; } = string.Empty;
private string CurrentLanguage { get; set; } = "ja";
@ -18,8 +17,6 @@ public partial class SongList
private SongBestResponse? response;
private UserSetting? userSetting;
private Dictionary<Difficulty, List<SongBestData>> songBestDataMap = new();
private readonly List<BreadcrumbItem> breadcrumbs = new();
private List<MusicDetail> musicMap = new();
@ -33,33 +30,18 @@ public partial class SongList
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
musicMap = GameDataService.GetMusicList();
CurrentLanguage = await JSRuntime.InvokeAsync<string>("blazorCulture.get");
CurrentLanguage = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
if (LoginService.IsLoggedIn && !LoginService.IsAdmin)
if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{
breadcrumbs.Add(new BreadcrumbItem("Dashboard", href: "/"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
}
else
{
breadcrumbs.Add(new BreadcrumbItem("Users", href: "/Users"));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users"));
};
breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem("Songs", href: $"/Users/{Baid}/Songs", disabled: false));
}
private async Task OnFavoriteToggled(SongBestData data)
{
var request = new SetFavoriteRequest
{
Baid = (uint)Baid,
IsFavorite = !data.IsFavorite,
SongId = data.SongId
};
var result = await Client.PostAsJsonAsync("api/FavoriteSongs", request);
if (result.IsSuccessStatusCode)
{
data.IsFavorite = !data.IsFavorite;
}
breadcrumbs.Add(new BreadcrumbItem(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false));
}
private bool FilterSongs(MusicDetail musicDetail)

View File

@ -1,54 +1,17 @@
@inject HttpClient Client
@inject IDialogService DialogService
@inject LoginService LoginService
@inject AuthService AuthService
@inject NavigationManager NavigationManager
@using TaikoWebUI.Components
@page "/Users"
<MudText Typo="Typo.h4">@Localizer["users"]</MudText>
<MudText Typo="Typo.h4">@Localizer["Users"]</MudText>
<MudGrid Class="my-8">
@if (response is not null)
{
// Response received and users are available
if (response.Users.Count != 0)
{
if (LoginService.IsAdmin || !LoginService.LoginRequired) // Admin mode, can see all users
{
@foreach (var user in response.Users)
{
<MudItem xs="12" md="6" lg="4">
<UserCard user="user" />
</MudItem>
}
}
else
{
// Not admin, redirect
@if (!LoginService.IsLoggedIn) // Not logged in, show login form
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
}
}
else
{ // No users in the database
<MudItem xs="12">
<MudText Align="Align.Center" Class="my-8">
@Localizer["No data."]
</MudText>
</MudItem>
}
} else
{
@if (!AuthService.LoginRequired || (AuthService.LoginRequired && AuthService.IsAdmin)) {
if (users == null) {
// Loading...
@for (uint i = 0; i < 6; i++)
{
for (uint i = 0; i < 6; i++) {
<MudItem xs="12" md="6" lg="4">
<MudCard Outlined="true">
<MudCardContent>
@ -65,5 +28,24 @@
</MudCard>
</MudItem>
}
} else if (users.Count > 0) {
foreach (var user in users)
{
<MudItem xs="12" md="6" lg="4">
<UserCard User="user" />
</MudItem>
}
} else { // No users in the database
<MudItem xs="12">
<MudText Align="Align.Center" Class="my-8">
@Localizer["No data."]
</MudText>
</MudItem>
}
} else if (AuthService.LoginRequired && !AuthService.IsLoggedIn) {
// Not logged in, redirect
NavigationManager.NavigateTo("/Login");
} else {
NavigationManager.NavigateTo("/");
}
</MudGrid>

View File

@ -2,11 +2,14 @@
public partial class Users
{
private DashboardResponse? response;
private List<User>? users;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
if (AuthService.IsAdmin || !AuthService.LoginRequired)
{
users = await Client.GetFromJsonAsync<List<User>>("api/Users");
}
}
}

View File

@ -19,7 +19,7 @@ builder.Services.AddSingleton<IGameDataService, GameDataService>();
builder.Services.Configure<WebUiSettings>(builder.Configuration.GetSection(nameof(WebUiSettings)));
builder.Services.AddScoped<LoginService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddLocalization();
builder.Services.AddSingleton<MudLocalizer, ResXMudLocalizer>();
builder.Services.AddSingleton<ScoreUtils>();

View File

@ -0,0 +1,240 @@
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Options;
using TaikoWebUI.Settings;
using Blazored.LocalStorage;
namespace TaikoWebUI.Services;
public sealed class AuthService
{
public event EventHandler? LoginStatusChanged;
public bool LoginRequired { get; }
public bool OnlyAdmin { get; }
private readonly int boundAccessCodeUpperLimit;
public bool RegisterWithLastPlayTime { get; }
public bool AllowUserDelete { get; }
public bool AllowFreeProfileEditing { get; }
public bool IsLoggedIn { get; private set; }
private uint LoggedInBaid { get; set; }
public bool IsAdmin { get; private set; }
private readonly ILocalStorageService localStorage;
private readonly HttpClient client;
public AuthService(IOptions<WebUiSettings> settings, ILocalStorageService localStorage, HttpClient client)
{
this.localStorage = localStorage;
IsLoggedIn = false;
IsAdmin = false;
var webUiSettings = settings.Value;
LoginRequired = webUiSettings.LoginRequired;
OnlyAdmin = webUiSettings.OnlyAdmin;
boundAccessCodeUpperLimit = webUiSettings.BoundAccessCodeUpperLimit;
RegisterWithLastPlayTime = webUiSettings.RegisterWithLastPlayTime;
AllowUserDelete = webUiSettings.AllowUserDelete;
AllowFreeProfileEditing = webUiSettings.AllowFreeProfileEditing;
this.client = client;
}
private void OnLoginStatusChanged()
{
LoginStatusChanged?.Invoke(this, EventArgs.Empty);
}
private static (uint, bool) GetBaidAndIsAdminFromToken(string authToken)
{
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(authToken);
var baid = uint.Parse(jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Name).Value);
var isAdmin = jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Role).Value == "Admin";
return (baid, isAdmin);
}
public async Task<int> Login(string inputAccessCode, string inputPassword)
{
// strip spaces or dashes from card number
inputAccessCode = inputAccessCode.Replace(" ", "").Replace("-", "").Replace(":", "");
var request = new LoginRequest
{
AccessCode = inputAccessCode,
Password = inputPassword
};
var responseMessage = await client.PostAsJsonAsync("api/Auth/Login", request);
if (!responseMessage.IsSuccessStatusCode)
{
// Unauthorized, extract specific error message as json
var responseContent = await responseMessage.Content.ReadAsStringAsync();
var responseJson = JsonSerializer.Deserialize<Dictionary<string, string>>(responseContent);
// Unknown error message
if (responseJson is null) return 5;
var errorMessage = responseJson["message"];
return errorMessage switch
{
"Access Code Not Found" => 3,
"User Not Registered" => 4,
"Invalid Password" => 2,
_ => 5
};
}
else
{
// Authorized, store Jwt token
var responseContent = await responseMessage.Content.ReadAsStringAsync();
var responseJson = JsonSerializer.Deserialize<Dictionary<string, string>>(responseContent);
if (responseJson is null) return 5;
var authToken = responseJson["authToken"];
await localStorage.SetItemAsync("authToken", authToken);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
var (baid, isAdmin) = GetBaidAndIsAdminFromToken(authToken);
IsLoggedIn = true;
IsAdmin = isAdmin;
LoggedInBaid = baid;
OnLoginStatusChanged();
return 1;
}
}
public async Task LoginWithAuthToken()
{
var hasAuthToken = await localStorage.ContainKeyAsync("authToken");
if (!hasAuthToken) return;
// Attempt to get JWT token from local storage
var authToken = await localStorage.GetItemAsync<string>("authToken");
if (authToken == null) return;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
var responseMessage = await client.PostAsync("api/Auth/LoginWithToken", null);
if (!responseMessage.IsSuccessStatusCode)
{
// Clear JWT token
await localStorage.RemoveItemAsync("authToken");
return;
}
var (baid, isAdmin) = GetBaidAndIsAdminFromToken(authToken);
IsLoggedIn = true;
IsAdmin = isAdmin;
LoggedInBaid = baid;
OnLoginStatusChanged();
}
public async Task<int> Register(string inputCardNum, DateTime inputDateTime, string inputPassword,
string inputConfirmPassword, string inviteCode)
{
if (OnlyAdmin) return 0;
if (inputPassword != inputConfirmPassword) return 2;
// strip spaces or dashes from card number
inputCardNum = inputCardNum.Replace(" ", "").Replace("-", "").Replace(":", "");
var request = new RegisterRequest
{
AccessCode = inputCardNum,
Password = inputPassword,
RegisterWithLastPlayTime = RegisterWithLastPlayTime,
LastPlayDateTime = inputDateTime,
InviteCode = inviteCode
};
var responseMessage = await client.PostAsJsonAsync("api/Auth/Register", request);
if (responseMessage.IsSuccessStatusCode) return 1;
// Unauthorized, extract specific error message as json
var responseContent = await responseMessage.Content.ReadAsStringAsync();
var responseJson = JsonSerializer.Deserialize<Dictionary<string, string>>(responseContent);
// Unknown error message
if (responseJson is null) return 6;
var errorMessage = responseJson["message"];
return errorMessage switch
{
"Access Code Not Found" => 3,
"User Already Registered" => 4,
"Wrong Last Play Time" => 5,
_ => 6
};
}
public async Task<int> ChangePassword(string inputAccessCode, string inputOldPassword, string inputNewPassword,
string inputConfirmNewPassword)
{
if (OnlyAdmin) return 0;
if (inputNewPassword != inputConfirmNewPassword) return 2;
var request = new ChangePasswordRequest
{
AccessCode = inputAccessCode,
OldPassword = inputOldPassword,
NewPassword = inputNewPassword
};
var responseMessage = await client.PostAsJsonAsync("api/Auth/ChangePassword", request);
if (responseMessage.IsSuccessStatusCode) return 1;
// Unauthorized, extract specific error message as json
var responseContent = await responseMessage.Content.ReadAsStringAsync();
var responseJson = JsonSerializer.Deserialize<Dictionary<string, string>>(responseContent);
// Unknown error message
if (responseJson is null) return 6;
var errorMessage = responseJson["message"];
return errorMessage switch
{
"Access Code Not Found" => 3,
"User Not Registered" => 5,
"Wrong Old Password" => 4,
_ => 6
};
}
public async Task Logout()
{
IsLoggedIn = false;
LoggedInBaid = 0;
IsAdmin = false;
// Clear JWT token
await localStorage.RemoveItemAsync("authToken");
OnLoginStatusChanged();
}
public async Task<User?> GetLoggedInUser()
{
return await client.GetFromJsonAsync<User>($"api/Users/{LoggedInBaid}");
}
public uint GetLoggedInBaid()
{
return LoggedInBaid;
}
public async Task<int> BindAccessCode(string inputAccessCode, User user)
{
if (inputAccessCode.Trim() == "") return 4; /*Empty access code*/
if (!IsLoggedIn && LoginRequired) return 0; /*User not connected and login is required*/
var loggedInUser = await GetLoggedInUser();
if (loggedInUser == null) return 0;
if (LoginRequired && !IsAdmin && user.Baid != loggedInUser.Baid) return 5; /*User not admin trying to update someone else's Access Codes*/
if (user.AccessCodes.Count >= boundAccessCodeUpperLimit) return 2; /*Limit of codes has been reached*/
var request = new BindAccessCodeRequest
{
AccessCode = inputAccessCode,
Baid = user.Baid
};
var responseMessage = await client.PostAsJsonAsync("api/Cards/BindAccessCode", request);
return responseMessage.IsSuccessStatusCode ? 1 : 3;
}
}

View File

@ -8,8 +8,6 @@ public class GameDataService : IGameDataService
{
private readonly HttpClient client;
private readonly Dictionary<uint, MusicDetail> musicMap = new();
private List<int> costumeFlagArraySizes = new();
private int titleFlagArraySize;
private ImmutableDictionary<uint, DanData> danMap = ImmutableDictionary<uint, DanData>.Empty;
private ImmutableHashSet<Title> titles = ImmutableHashSet<Title>.Empty;
@ -19,6 +17,23 @@ public class GameDataService : IGameDataService
private string[] kigurumiTitles = { };
private string[] puchiTitles = { };
private List<uint> kigurumiUniqueIdList = new();
private List<uint> headUniqueIdList = new();
private List<uint> bodyUniqueIdList = new();
private List<uint> faceUniqueIdList = new();
private List<uint> puchiUniqueIdList = new();
private List<uint> titleUniqueIdList = new();
private List<uint> titlePlateIdList = new();
private List<uint> lockedKigurumiUniqueIdList = new();
private List<uint> lockedHeadUniqueIdList = new();
private List<uint> lockedBodyUniqueIdList = new();
private List<uint> lockedFaceUniqueIdList = new();
private List<uint> lockedPuchiUniqueIdList = new();
private List<uint> lockedTitleUniqueIdList = new();
private List<uint> lockedTitlePlateIdList = new();
public GameDataService(HttpClient client)
{
this.client = client;
@ -38,19 +53,30 @@ public class GameDataService : IGameDataService
danMap = danData.ToImmutableDictionary(data => data.DanId);
// To prevent duplicate entries in wordlist
var dict = wordList.WordListEntries.GroupBy(entry => entry.Key)
var wordlistDict = wordList.WordListEntries.GroupBy(entry => entry.Key)
.ToImmutableDictionary(group => group.Key, group => group.First());
await Task.Run(() => InitializeMusicMap(musicInfo, dict, musicOrder));
await Task.Run(() => InitializeMusicMap(musicInfo, wordlistDict, musicOrder));
InitializeCostumeFlagArraySizes(donCosRewardData);
InitializeTitleFlagArraySize(shougouData);
await Task.Run(() => InitializeCostumeIdLists(donCosRewardData));
await Task.Run(() => InitializeTitleIdList(shougouData));
await Task.Run(() => InitializeHeadTitles(dict));
await Task.Run(() => InitializeFaceTitles(dict));
await Task.Run(() => InitializeBodyTitles(dict));
await Task.Run(() => InitializePuchiTitles(dict));
await Task.Run(() => InitializeKigurumiTitles(dict));
await Task.Run(() => InitializeTitles(dict, shougouData));
await Task.Run(() => InitializeHeadTitles(wordlistDict));
await Task.Run(() => InitializeFaceTitles(wordlistDict));
await Task.Run(() => InitializeBodyTitles(wordlistDict));
await Task.Run(() => InitializePuchiTitles(wordlistDict));
await Task.Run(() => InitializeKigurumiTitles(wordlistDict));
await Task.Run(() => InitializeTitles(wordlistDict, shougouData));
var lockedCostumeDataDictionary = await client.GetFromJsonAsync<Dictionary<string, List<uint>>>($"{dataBaseUrl}/data/locked_costume_data.json") ?? throw new InvalidOperationException();
lockedKigurumiUniqueIdList = lockedCostumeDataDictionary.GetValueOrDefault("Kigurumi") ?? new List<uint>();
lockedHeadUniqueIdList = lockedCostumeDataDictionary.GetValueOrDefault("Head") ?? new List<uint>();
lockedBodyUniqueIdList = lockedCostumeDataDictionary.GetValueOrDefault("Body") ?? new List<uint>();
lockedFaceUniqueIdList = lockedCostumeDataDictionary.GetValueOrDefault("Face") ?? new List<uint>();
lockedPuchiUniqueIdList = lockedCostumeDataDictionary.GetValueOrDefault("Puchi") ?? new List<uint>();
var lockedTitleDataDictionary = await client.GetFromJsonAsync<Dictionary<string, List<uint>>>($"{dataBaseUrl}/data/locked_title_data.json") ?? throw new InvalidOperationException();
lockedTitleUniqueIdList = lockedTitleDataDictionary.GetValueOrDefault("TitleNo") ?? new List<uint>();
lockedTitlePlateIdList = lockedTitleDataDictionary.GetValueOrDefault("TitlePlateNo") ?? new List<uint>();
}
private async Task<T> GetData<T>(string dataBaseUrl, string fileBaseName) where T : notnull
@ -151,15 +177,45 @@ public class GameDataService : IGameDataService
return titles;
}
public List<int> GetCostumeFlagArraySizes()
public List<uint> GetKigurumiUniqueIdList()
{
return costumeFlagArraySizes;
return kigurumiUniqueIdList;
}
private void InitializeTitleFlagArraySize(Shougous? shougouData)
public List<uint> GetHeadUniqueIdList()
{
return headUniqueIdList;
}
public List<uint> GetBodyUniqueIdList()
{
return bodyUniqueIdList;
}
public List<uint> GetFaceUniqueIdList()
{
return faceUniqueIdList;
}
public List<uint> GetPuchiUniqueIdList()
{
return puchiUniqueIdList;
}
public List<uint> GetTitleUniqueIdList()
{
return titleUniqueIdList;
}
public List<uint> GetTitlePlateIdList()
{
return titlePlateIdList;
}
private void InitializeTitleIdList(Shougous? shougouData)
{
shougouData.ThrowIfNull("Shouldn't happen!");
titleFlagArraySize = (int)shougouData.ShougouEntries.Max(entry => entry.UniqueId) + 1;
titleUniqueIdList = shougouData.ShougouEntries.Select(entry => entry.UniqueId).ToList();
}
private void InitializeTitles(ImmutableDictionary<string, WordListEntry> dict, Shougous? shougouData)
@ -167,7 +223,7 @@ public class GameDataService : IGameDataService
shougouData.ThrowIfNull("Shouldn't happen!");
var set = ImmutableHashSet.CreateBuilder<Title>();
for (var i = 1; i < titleFlagArraySize; i++)
foreach (var i in titleUniqueIdList)
{
var key = $"syougou_{i}";
@ -178,6 +234,11 @@ public class GameDataService : IGameDataService
.Select(entry => entry.Rarity)
.FirstOrDefault();
if (!titlePlateIdList.Contains(titleRarity))
{
titlePlateIdList.Add(titleRarity);
}
set.Add(new Title
{
TitleName = titleWordlistItem.JapaneseText,
@ -189,40 +250,30 @@ public class GameDataService : IGameDataService
titles = set.ToImmutable();
}
private void InitializeCostumeFlagArraySizes(DonCosRewards? donCosRewardData)
private void InitializeCostumeIdLists(DonCosRewards? donCosRewardData)
{
donCosRewardData.ThrowIfNull("Shouldn't happen!");
var kigurumiUniqueIdList = donCosRewardData.DonCosRewardEntries
kigurumiUniqueIdList = donCosRewardData.DonCosRewardEntries
.Where(entry => entry.CosType == "kigurumi")
.Select(entry => entry.UniqueId);
var headUniqueIdList = donCosRewardData.DonCosRewardEntries
.Select(entry => entry.UniqueId).ToList();
headUniqueIdList = donCosRewardData.DonCosRewardEntries
.Where(entry => entry.CosType == "head")
.Select(entry => entry.UniqueId);
var bodyUniqueIdList = donCosRewardData.DonCosRewardEntries
.Select(entry => entry.UniqueId).ToList();
bodyUniqueIdList = donCosRewardData.DonCosRewardEntries
.Where(entry => entry.CosType == "body")
.Select(entry => entry.UniqueId);
var faceUniqueIdList = donCosRewardData.DonCosRewardEntries
.Select(entry => entry.UniqueId).ToList();
faceUniqueIdList = donCosRewardData.DonCosRewardEntries
.Where(entry => entry.CosType == "face")
.Select(entry => entry.UniqueId);
var puchiUniqueIdList = donCosRewardData.DonCosRewardEntries
.Select(entry => entry.UniqueId).ToList();
puchiUniqueIdList = donCosRewardData.DonCosRewardEntries
.Where(entry => entry.CosType == "puchi")
.Select(entry => entry.UniqueId);
costumeFlagArraySizes = new List<int>
{
(int)kigurumiUniqueIdList.Max() + 1,
(int)headUniqueIdList.Max() + 1,
(int)bodyUniqueIdList.Max() + 1,
(int)faceUniqueIdList.Max() + 1,
(int)puchiUniqueIdList.Max() + 1
};
.Select(entry => entry.UniqueId).ToList();
}
private void InitializeKigurumiTitles(ImmutableDictionary<string, WordListEntry> dict)
{
var costumeKigurumiMax = costumeFlagArraySizes[0];
kigurumiTitles = new string[costumeKigurumiMax];
for (var i = 0; i < costumeKigurumiMax; i++)
kigurumiTitles = new string[kigurumiUniqueIdList.Max() + 1];
foreach (var i in kigurumiUniqueIdList)
{
var key = $"costume_kigurumi_{i}";
@ -233,9 +284,8 @@ public class GameDataService : IGameDataService
private void InitializeHeadTitles(ImmutableDictionary<string, WordListEntry> dict)
{
var costumeHeadMax = costumeFlagArraySizes[1];
headTitles = new string[costumeHeadMax];
for (var i = 0; i < costumeHeadMax; i++)
headTitles = new string[headUniqueIdList.Max() + 1];
foreach (var i in headUniqueIdList)
{
var key = $"costume_head_{i}";
@ -246,9 +296,8 @@ public class GameDataService : IGameDataService
private void InitializeBodyTitles(ImmutableDictionary<string, WordListEntry> dict)
{
var costumeBodyMax = costumeFlagArraySizes[2];
bodyTitles = new string[costumeBodyMax];
for (var i = 0; i < costumeBodyMax; i++)
bodyTitles = new string[bodyUniqueIdList.Max() + 1];
foreach (var i in bodyUniqueIdList)
{
var key = $"costume_body_{i}";
@ -259,9 +308,8 @@ public class GameDataService : IGameDataService
private void InitializeFaceTitles(ImmutableDictionary<string, WordListEntry> dict)
{
var costumeFaceMax = costumeFlagArraySizes[3];
faceTitles = new string[costumeFaceMax];
for (var i = 0; i < costumeFaceMax; i++)
faceTitles = new string[faceUniqueIdList.Max() + 1];
foreach (var i in faceUniqueIdList)
{
var key = $"costume_face_{i}";
@ -272,9 +320,8 @@ public class GameDataService : IGameDataService
private void InitializePuchiTitles(ImmutableDictionary<string, WordListEntry> dict)
{
var costumePuchiMax = costumeFlagArraySizes[4];
puchiTitles = new string[costumePuchiMax];
for (var i = 0; i < costumePuchiMax; i++)
puchiTitles = new string[puchiUniqueIdList.Max() + 1];
foreach (var i in puchiUniqueIdList)
{
var key = $"costume_puchi_{i}";
@ -322,4 +369,39 @@ public class GameDataService : IGameDataService
}
}
}
public List<uint> GetLockedKigurumiUniqueIdList()
{
return lockedKigurumiUniqueIdList;
}
public List<uint> GetLockedHeadUniqueIdList()
{
return lockedHeadUniqueIdList;
}
public List<uint> GetLockedBodyUniqueIdList()
{
return lockedBodyUniqueIdList;
}
public List<uint> GetLockedFaceUniqueIdList()
{
return lockedFaceUniqueIdList;
}
public List<uint> GetLockedPuchiUniqueIdList()
{
return lockedPuchiUniqueIdList;
}
public List<uint> GetLockedTitleUniqueIdList()
{
return lockedTitleUniqueIdList;
}
public List<uint> GetLockedTitlePlateIdList()
{
return lockedTitlePlateIdList;
}
}

View File

@ -27,7 +27,21 @@ public interface IGameDataService
public string GetFaceTitle(uint index);
public string GetPuchiTitle(uint index);
public List<int> GetCostumeFlagArraySizes();
public List<uint> GetKigurumiUniqueIdList();
public List<uint> GetHeadUniqueIdList();
public List<uint> GetBodyUniqueIdList();
public List<uint> GetFaceUniqueIdList();
public List<uint> GetPuchiUniqueIdList();
public List<uint> GetTitleUniqueIdList();
public List<uint> GetTitlePlateIdList();
public List<uint> GetLockedKigurumiUniqueIdList();
public List<uint> GetLockedHeadUniqueIdList();
public List<uint> GetLockedBodyUniqueIdList();
public List<uint> GetLockedFaceUniqueIdList();
public List<uint> GetLockedPuchiUniqueIdList();
public List<uint> GetLockedTitleUniqueIdList();
public List<uint> GetLockedTitlePlateIdList();
public ImmutableHashSet<Title> GetTitles();
}

View File

@ -1,187 +0,0 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using TaikoWebUI.Settings;
namespace TaikoWebUI.Services;
public class LoginService
{
public event EventHandler? LoginStatusChanged;
public delegate void LoginStatusChangedEventHandler(object? sender, EventArgs e);
public bool LoginRequired { get; }
public bool OnlyAdmin { get; }
private readonly int boundAccessCodeUpperLimit;
public bool RegisterWithLastPlayTime { get; }
public bool AllowUserDelete { get; }
public bool AllowFreeProfileEditing { get; }
public LoginService(IOptions<WebUiSettings> settings)
{
IsLoggedIn = false;
IsAdmin = false;
var webUiSettings = settings.Value;
LoginRequired = webUiSettings.LoginRequired;
OnlyAdmin = webUiSettings.OnlyAdmin;
boundAccessCodeUpperLimit = webUiSettings.BoundAccessCodeUpperLimit;
RegisterWithLastPlayTime = webUiSettings.RegisterWithLastPlayTime;
AllowUserDelete = webUiSettings.AllowUserDelete;
AllowFreeProfileEditing = webUiSettings.AllowFreeProfileEditing;
}
public bool IsLoggedIn { get; private set; }
private User LoggedInUser { get; set; } = new();
public bool IsAdmin { get; private set; }
protected virtual void OnLoginStatusChanged()
{
LoginStatusChanged?.Invoke(this, EventArgs.Empty);
}
public int Login(string inputCardNum, string inputPassword, DashboardResponse response)
{
// strip spaces or dashes from card number
inputCardNum = inputCardNum.Replace(" ", "").Replace("-", "");
foreach (var user in response.Users.Where(user => user.AccessCodes.Contains(inputCardNum)))
{
foreach (var userCredential in response.UserCredentials.Where(userCredential => userCredential.Baid == user.Baid))
{
if (userCredential.Password == "") return 4;
if (ComputeHash(inputPassword, userCredential.Salt) != userCredential.Password) return 2;
IsAdmin = user.IsAdmin;
if (!IsAdmin && OnlyAdmin) return 0;
IsLoggedIn = true;
LoggedInUser = user;
OnLoginStatusChanged();
return 1;
}
}
return 3;
}
public async Task<int> Register(string inputCardNum, DateTime inputDateTime, string inputPassword, string inputConfirmPassword,
DashboardResponse response, HttpClient client)
{
if (OnlyAdmin) return 0;
// strip spaces or dashes from card number
inputCardNum = inputCardNum.Replace(" ", "").Replace("-", "");
foreach (var user in response.Users.Where(user => user.AccessCodes.Contains(inputCardNum)))
{
foreach (var userCredential in response.UserCredentials.Where(userCredential => userCredential.Baid == user.Baid))
{
if (RegisterWithLastPlayTime)
{
var userSettingResponse = await client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{user.Baid}");
if (userSettingResponse is null) return 3;
var lastPlayDateTime = userSettingResponse.LastPlayDateTime;
var diffMinutes = (inputDateTime - lastPlayDateTime).Duration().TotalMinutes;
if (diffMinutes > 5) return 5;
}
if (userCredential.Password != "") return 4;
if (inputPassword != inputConfirmPassword) return 2;
var salt = CreateSalt();
var request = new SetPasswordRequest
{
Baid = user.Baid,
Password = ComputeHash(inputPassword, salt),
Salt = salt
};
var responseMessage = await client.PostAsJsonAsync("api/Credentials", request);
return responseMessage.IsSuccessStatusCode ? 1 : 3;
}
}
return 3;
}
private static string CreateSalt()
{
//Generate a cryptographic random number.
var randomNumber = new byte[32];
var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
var salt = Convert.ToBase64String(randomNumber);
// Return a Base64 string representation of the random number.
return salt;
}
private static string ComputeHash(string inputPassword, string salt)
{
var encDataByte = Encoding.UTF8.GetBytes(inputPassword + salt);
var encodedData = Convert.ToBase64String(encDataByte);
encDataByte = Encoding.UTF8.GetBytes(encodedData);
encodedData = Convert.ToBase64String(encDataByte);
encDataByte = Encoding.UTF8.GetBytes(encodedData);
encodedData = Convert.ToBase64String(encDataByte);
encDataByte = Encoding.UTF8.GetBytes(encodedData);
encodedData = Convert.ToBase64String(encDataByte);
return encodedData;
}
public async Task<int> ChangePassword(string inputCardNum, string inputOldPassword, string inputNewPassword,
string inputConfirmNewPassword, DashboardResponse response, HttpClient client)
{
if (OnlyAdmin) return 0;
foreach (var user in response.Users.Where(user => user.AccessCodes.Contains(inputCardNum)))
{
foreach (var userCredential in response.UserCredentials.Where(userCredential => userCredential.Baid == user.Baid))
{
if (userCredential.Password != ComputeHash(inputOldPassword, userCredential.Salt)) return 4;
if (inputNewPassword != inputConfirmNewPassword) return 2;
var request = new SetPasswordRequest
{
Baid = user.Baid,
Password = ComputeHash(inputNewPassword, userCredential.Salt),
Salt = userCredential.Salt
};
var responseMessage = await client.PostAsJsonAsync("api/Credentials", request);
return responseMessage.IsSuccessStatusCode ? 1 : 3;
}
}
return 3;
}
public void Logout()
{
IsLoggedIn = false;
LoggedInUser = new User();
IsAdmin = false;
OnLoginStatusChanged();
}
public User GetLoggedInUser()
{
return LoggedInUser;
}
public void ResetLoggedInUser(DashboardResponse? response)
{
if (response is null) return;
var baid = LoggedInUser.Baid;
var newLoggedInUser = response.Users.FirstOrDefault(u => u.Baid == baid);
if (newLoggedInUser is null) return;
LoggedInUser = newLoggedInUser;
}
public async Task<int> BindAccessCode(string inputAccessCode, User user, HttpClient client)
{
if (inputAccessCode.Trim() == "") return 4; /*Empty access code*/
if (!IsLoggedIn && LoginRequired) return 0; /*User not connected and login is required*/
if (LoginRequired && !IsAdmin && !(user.Baid == GetLoggedInUser().Baid)) return 5; /*User not admin trying to update someone elses Access Codes*/
if (user.AccessCodes.Count >= boundAccessCodeUpperLimit) return 2; /*Limit of codes has been reached*/
var request = new BindAccessCodeRequest
{
AccessCode = inputAccessCode,
Baid = user.Baid
};
var responseMessage = await client.PostAsJsonAsync("api/Cards", request);
return responseMessage.IsSuccessStatusCode ? 1 : 3;
}
}

View File

@ -2,7 +2,7 @@
public class Title
{
public int TitleId { get; set; }
public uint TitleId { get; set; }
public string TitleName { get; init; } = string.Empty;

View File

@ -14,15 +14,17 @@
<ItemGroup>
<PackageReference Include="Autocomplete.Clients" Version="1.1.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="6.5.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0-rc.1.23421.29" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0-rc.1.23421.29" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0-rc.1.23419.4" />
<PackageReference Include="MudBlazor" Version="6.17.0" />
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="6.9.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="MudBlazor" Version="6.19.1" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Swan.Core" Version="7.0.0-beta.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
</ItemGroup>
<ItemGroup>
@ -66,6 +68,12 @@
<_ContentIncludedByDefault Remove="Pages\Pages\Profile.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Register.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Users.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\OTPDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\ResetPasswordConfirmDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Login.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\PlayHistory.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Song.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\SongList.razor" />
</ItemGroup>
<ItemGroup>
@ -103,6 +111,15 @@
</Compile>
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Pages\Pages\AccessCode.razor" />
<UpToDateCheckInput Remove="Pages\Pages\ChangePassword.razor" />
<UpToDateCheckInput Remove="Pages\Pages\DaniDojo.razor" />
<UpToDateCheckInput Remove="Pages\Pages\Dashboard.razor" />
<UpToDateCheckInput Remove="Pages\Pages\Dialogs\AccessCodeDeleteConfirmDialog.razor" />
<UpToDateCheckInput Remove="Pages\Pages\Dialogs\ChooseTitleDialog.razor" />
</ItemGroup>
</Project>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,30 +14,42 @@
</head>
<body>
<div id="app">
<div id="app">
<div class="loader-container">
<div class="linear-progress"></div>
<div class="loading-text">
Loading...
</div>
</div>
</div>
</div>
<div id="blazor-error-ui">
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="js/textFit.min.js"></script>
<script src="js/updateTextFit.js"></script>
<script>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="js/textFit.min.js"></script>
<script src="js/updateTextFit.js"></script>
<script>
window.blazorCulture = {
get: () => localStorage['BlazorCulture'],
set: (value) => localStorage['BlazorCulture'] = value
};
</script>
</script>
<script>
window.clipboardCopy = {
copyText: function(text) {
navigator.clipboard.writeText(text).then(function () {
alert("Copied to clipboard!");
})
.catch(function (error) {
console.error("Failed to copy text: ", error);
});
}
};
</script>
</body>
</html>