1
0
mirror of synced 2024-11-15 02:47:35 +01:00

Implemented better authentication system, completed more localization, implemented cached JWT login, implemented locking costumes and title plates for FreeProfileEditing

This commit is contained in:
S-Sebb 2024-05-01 16:13:47 +01:00
parent 19db91fc59
commit 650552e403
66 changed files with 4233 additions and 868 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

@ -3,6 +3,4 @@
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,221 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using TaikoLocalServer.Settings;
using OtpNet;
using SharedProject.Models.Requests;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class AuthController(ICredentialService credentialService, ICardService cardService,
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")]
public async Task<IActionResult> Login(LoginRequest loginRequest)
{
var accessCode = loginRequest.AccessCode;
var password = loginRequest.Password;
var card = await cardService.GetCardByAccessCode(accessCode);
if (card == null)
return Unauthorized(new { message = "Access Code Not Found" });
var credential = await credentialService.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("Register")]
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 cardService.GetCardByAccessCode(accessCode);
if (card == null)
return Unauthorized(new { message = "Access Code Not Found" });
var credential = await credentialService.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 credentialService.UpdatePassword(card.Baid, hashedPassword, salt);
return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" });
}
[HttpPost("ChangePassword")]
public async Task<IActionResult> ChangePassword(ChangePasswordRequest changePasswordRequest)
{
var accessCode = changePasswordRequest.AccessCode;
var oldPassword = changePasswordRequest.OldPassword;
var newPassword = changePasswordRequest.NewPassword;
var card = await cardService.GetCardByAccessCode(accessCode);
if (card == null)
return Unauthorized(new { message = "Access Code Not Found" });
var credential = await credentialService.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 credentialService.UpdatePassword(card.Baid, hashedNewPassword, salt);
return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" });
}
[HttpPost("ResetPassword")]
public async Task<IActionResult> ResetPassword(ResetPasswordRequest resetPasswordRequest)
{
var baid = resetPasswordRequest.Baid;
var credential = await credentialService.GetCredentialByBaid(baid);
if (credential == null)
return Unauthorized(new { message = "Credential Not Found" });
var result = await credentialService.UpdatePassword(baid, "", "");
return result ? Ok() : Unauthorized( new { message = "Failed to Reset Password" });
}
[HttpPost("GenerateOtp")]
public IActionResult GenerateOtp(GenerateOtpRequest generateOtpRequest)
{
var totp = MakeTotp(generateOtpRequest.Baid);
return Ok(new { otp = totp.ComputeTotp() });
}
[HttpPost("VerifyOtp")]
public IActionResult VerifyOtpHandler(VerifyOtpRequest verifyOtpRequest)
{
if (VerifyOtp(verifyOtpRequest.Otp, verifyOtpRequest.Baid))
return Ok();
return Unauthorized();
}
}

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

@ -7,23 +7,19 @@ namespace TaikoLocalServer.Controllers.Api;
public class DashboardController : BaseController<DashboardController>
{
private readonly ICardService cardService;
private readonly ICredentialService credentialService;
public DashboardController(ICardService cardService, ICredentialService credentialService)
public DashboardController(ICardService cardService)
{
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
Users = users
};
}

View File

@ -0,0 +1,54 @@
using GameDatabase.Entities;
using SharedProject.Models;
using SharedProject.Models.Responses;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class PlayHistoryController(
IUserDatumService userDatumService,
ISongBestDatumService songBestDatumService,
ISongPlayDatumService songPlayDatumService)
: BaseController<PlayDataController>
{
private readonly ISongBestDatumService songBestDatumService = songBestDatumService;
[HttpGet("{baid}")]
public async Task<ActionResult<SongHistoryResponse>> GetSongHistory(uint baid)
{
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,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;
@ -39,6 +42,7 @@ 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.Host.UseSerilog((context, configuration) =>
{
@ -65,6 +69,27 @@ 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)));
// 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.AddControllers().AddProtoBufNet();
builder.Services.AddDbContext<TaikoDbContext>(option =>
{
@ -125,13 +150,12 @@ try
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseHttpLogging();
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode >= 400)
{
Log.Error("Unknown request from: {RemoteIpAddress} {Method} {Path} {StatusCode}",

View File

@ -4,15 +4,8 @@ using Swan.Mapping;
namespace TaikoLocalServer.Services;
public class CredentialService : ICredentialService
public class CredentialService(TaikoDbContext context) : 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();
@ -48,4 +41,9 @@ public class CredentialService : ICredentialService
return true;
}
public async Task<Credential?> GetCredentialByBaid(uint baid)
{
return await context.Credentials.FindAsync(baid);
}
}

View File

@ -4,11 +4,13 @@ namespace TaikoLocalServer.Services.Interfaces;
public interface ICredentialService
{
public Task<List<UserCredential>> GetUserCredentialsFromCredentials();
public Task<List<UserCredential>> GetUserCredentialsFromCredentials();
public Task AddCredential(Credential credential);
public Task AddCredential(Credential credential);
public Task<bool> DeleteCredential(uint baid);
public Task<bool> DeleteCredential(uint baid);
public Task<bool> UpdatePassword(uint baid, string password, string salt);
public Task<bool> UpdatePassword(uint baid, string password, string salt);
public Task<Credential?> GetCredentialByBaid(uint baid);
}

View File

@ -0,0 +1,10 @@
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;
}

View File

@ -6,171 +6,183 @@
<ImplicitUsings>enable</ImplicitUsings>
<Version>1.1.0</Version>
<LangVersion>12</LangVersion>
<EnableConfigurationBindingGenerator>false</EnableConfigurationBindingGenerator>
<EnableConfigurationBindingGenerator>false</EnableConfigurationBindingGenerator>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Templates\TemplateController.cs" />
<Compile Remove="Templates\TemplateController.cs" />
</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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<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>
<None Update="Certificates\cert.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Certificates\root.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\ServerSettings.json" />
<None Include="Configurations\ServerSettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\Database.json" />
<None Include="Configurations\Database.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\DataSettings.json" />
<None Include="Configurations\DataSettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\Kestrel.json" />
<None Include="Configurations\Kestrel.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\Logging.json" />
<None Include="Configurations\Logging.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Update="wwwroot\data\locked_songs_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\movie_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\musicinfo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\shop_folder_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\token_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\wordlist.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\gaiden_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\qrcode_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\don_cos_reward.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\don_cos_reward.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\shougou.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\shougou.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\neiro.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\neiro.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\don_cos_reward.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\neiro.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\shougou.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\don_cos_reward.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\neiro.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\shougou.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Update="Certificates\cert.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Certificates\root.pfx">
<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>
<Content Remove="Configurations\Database.json" />
<None Include="Configurations\Database.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\DataSettings.json" />
<None Include="Configurations\DataSettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\Kestrel.json" />
<None Include="Configurations\Kestrel.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Remove="Configurations\Logging.json" />
<None Include="Configurations\Logging.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Update="wwwroot\data\locked_songs_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\movie_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\musicinfo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\shop_folder_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\token_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\wordlist.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\gaiden_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\qrcode_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\don_cos_reward.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\don_cos_reward.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\shougou.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\shougou.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\neiro.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\neiro.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\don_cos_reward.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\neiro.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\shougou.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<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>
<Content Update="wwwroot\data\datatable-bak\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\neiro.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\shougou.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable-bak\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\data\dan_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\event_folder_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\intro_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\dan_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\event_folder_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\intro_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GameDatabase\GameDatabase.csproj" />
<ProjectReference Include="..\SharedProject\SharedProject.csproj" />
<ProjectReference Include="..\GameDatabase\GameDatabase.csproj" />
<ProjectReference Include="..\SharedProject\SharedProject.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<ProjectReference Include="..\TaikoWebUI\TaikoWebUI.csproj" />

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 LoginService LoginService
<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,61 @@
</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 (LoginService.LoginRequired)
{
// If not logged in, attempt to use JwtToken from local storage to log in
var hasJwtToken = await LocalStorage.ContainKeyAsync("authToken");
if (hasJwtToken)
{
var authToken = await LocalStorage.GetItemAsync<string>("authToken");
if (!string.IsNullOrWhiteSpace(authToken))
{
// Attempt to log in with the token
var loginResult = await LoginService.LoginWithAuthToken(authToken, Client);
if (!loginResult)
{
// Failed to log in with the token, remove it
await LocalStorage.RemoveItemAsync("authToken");
}
}
}
}
}
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

@ -5,7 +5,7 @@
@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>
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">@Localizer["Dashboard"]</MudNavLink>
@if (LoginService.IsAdmin || !LoginService.LoginRequired)
{
<MudNavLink Href="/Users" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.People">@Localizer["Users"]</MudNavLink>
@ -24,12 +24,13 @@
{
<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>
<MudNavGroup Title=@Localizer["Play Data"] Expanded="true" Icon="@Icons.Material.Filled.EmojiEvents">
<MudNavLink Href="@($"Users/{currentUser.Baid}/Songs")" Match="NavLinkMatch.All">@Localizer["Song List"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/HighScores")" Match="NavLinkMatch.All">@Localizer["High Scores"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/PlayHistory")" Match="NavLinkMatch.All">@Localizer["Play History"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.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>
@ -73,7 +74,7 @@
};
var options = new DialogOptions() { DisableBackdropClick = true };
DialogService.Show<UserQrCodeDialog>("QR Code", parameters, options);
DialogService.Show<UserQrCodeDialog>(@Localizer["QR Code"], parameters, options);
// Prevent the settings menu from closing
_settingsOpen = true;

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="SongPlayDatumDto" SortBy="x => x.PlayTime">
@Localizer["Play Time"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.Difficulty">
@Localizer["Difficulty"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.Crown">
@Localizer["Crown"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.ScoreRank">
@Localizer["Rank"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.Score">
@Localizer["Score"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.GoodCount">
@Localizer["Good"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.OkCount">
@Localizer["OK"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.MissCount">
@Localizer["Bad"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.DrumrollCount">
@Localizer["Drumroll"]
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="SongPlayDatumDto" 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<SongPlayDatumDto> Items { get; set; } = new();
private const string IconStyle = "width:25px; height:25px;";
}
}

View File

@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Components;
@using System.Text.Json
@using Microsoft.AspNetCore.Components;
@using TaikoWebUI.Pages.Dialogs;
@inject TaikoWebUI.Utilities.StringUtil StringUtil;
@inject IDialogService DialogService;
@ -26,7 +27,7 @@
<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"
@ -54,6 +55,16 @@
<MudDivider />
}
@if (LoginService.LoginRequired && LoginService.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 (LoginService.LoginRequired && LoginService.IsAdmin)
{
<MudMenuItem Icon="@Icons.Material.Filled.LockReset"
OnClick="@(_ => ResetPassword(user))"
@ -76,7 +87,7 @@
</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)
{
@ -91,7 +102,12 @@
</MudText>
@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>
@ -104,19 +120,20 @@
<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>
</MudMenu>
</MudStack>
</MudCardActions>
</MudCard>
<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>
</MudCard>
}
@code {
@ -139,7 +156,7 @@
};
var options = new DialogOptions() { DisableBackdropClick = true };
DialogService.Show<UserQrCodeDialog>("QR Code", parameters, options);
DialogService.Show<UserQrCodeDialog>(Localizer["QR Code"], parameters, options);
return Task.CompletedTask;
}
@ -150,9 +167,9 @@
if (LoginService.LoginRequired && !LoginService.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,7 +177,7 @@
["user"] = user
};
var dialog = DialogService.Show<ResetPasswordConfirmDialog>("Reset Password", parameters, options);
var dialog = DialogService.Show<ResetPasswordConfirmDialog>(Localizer["Reset Password"], parameters, options);
var result = await dialog.Result;
if (result.Canceled) return;
@ -174,9 +191,9 @@
if (!LoginService.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,7 +201,7 @@
["user"] = user
};
var dialog = DialogService.Show<UserDeleteConfirmDialog>("Delete User", parameters, options);
var dialog = DialogService.Show<UserDeleteConfirmDialog>(Localizer["Delete User"], parameters, options);
var result = await dialog.Result;
if (result.Canceled) return;
@ -193,4 +210,46 @@
LoginService.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 };
DialogService.Show<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,572 @@ 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 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 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 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 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 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 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,199 @@
<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>
</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,205 @@
<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>
</root>

View File

@ -120,4 +120,253 @@
<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>
</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,202 @@
<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>
</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,202 @@
<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>
</root>

View File

@ -21,20 +21,20 @@
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>
@ -50,7 +50,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++)
{

View File

@ -26,14 +26,14 @@ public partial class AccessCode
if (LoginService.IsLoggedIn && !LoginService.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()

View File

@ -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

@ -26,37 +26,50 @@ public partial class ChangePassword
{
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

@ -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">

View File

@ -31,48 +31,48 @@ public partial class DaniDojo
if (LoginService.IsLoggedIn && !LoginService.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["Failed"],
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;
@ -134,30 +134,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

@ -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

@ -2,9 +2,7 @@
public partial class ResetPasswordConfirmDialog
{
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public User User { get; set; } = new();
@ -13,14 +11,12 @@ public partial class ResetPasswordConfirmDialog
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

@ -5,7 +5,7 @@
<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

@ -50,14 +50,14 @@ public partial class HighScores
// Breadcrumbs
if (LoginService.IsLoggedIn && !LoginService.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)

View File

@ -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

@ -17,15 +17,15 @@ public partial class Login
{
if (response != null)
{
var result = LoginService.Login(inputAccessCode, inputPassword, response);
var options = new DialogOptions() { DisableBackdropClick = true };
var result = await LoginService.Login(inputAccessCode, inputPassword, Client);
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,23 +33,29 @@ 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,145 @@
@inject IGameDataService GameDataService
@inject HttpClient Client
@inject LoginService LoginService
@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 (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin)))
{
<MudItem xs="12">
<MudText Align="Align.Center" Class="my-8">
@Localizer["Log In First"]
</MudText>
</MudItem>
}
else
{
<MudItem xs="12">
<MudTable Items="@songHistoryDataMap.Values.ToList()" Elevation="0" Outlined="true" Filter=@FilterSongs Virtualize="true" RowsPerPage="25">
<ToolBarContent>
<MudGrid Spacing="2">
<MudItem xs="12" md="4">
<MudText Typo="Typo.h6">@Localizer["Total Plays"]:@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>
<MudTd Style="width:400px">
<MudText>
@CultureInfo.CurrentCulture.TextInfo.ToTitleCase(context[0].PlayTime.ToString(Localizer["DateFormat"]))
</MudText>
</MudTd>
</RowTemplate>
<ChildRowContent>
<MudTable Items="@context" T="SongHistoryData" Context="songHistoryData" Elevation="0" Striped="false" Hover="false" ReadOnly="true">
<HeaderContent>
<MudTh>@Localizer["Difficulty"]</MudTh>
<MudTh>@Localizer["Level"]</MudTh>
<MudTh>@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>
<MudTh>@Localizer["Song Number"]</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">
<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>
</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>
<MudTd>@(songHistoryData.SongNumber + 1)</MudTd>
</RowTemplate>
</MudTable>
</ChildRowContent>
<PagerContent>
<MudTablePager />
</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

@ -124,100 +124,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++)
{
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++)
{
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++)
{
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++)
{
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++)
{
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>
}
<MudSelect @bind-Value="@response.Head" Label=@Localizer["Head"]>
@foreach (var index in headUniqueIdList)
{
var costumeTitle = GameDataService.GetHeadTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Body" Label=@Localizer["Body"]>
@foreach (var index in bodyUniqueIdList)
{
var costumeTitle = GameDataService.GetBodyTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Face" Label=@Localizer["Face"]>
@foreach (var index in faceUniqueIdList)
{
var costumeTitle = GameDataService.GetFaceTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Kigurumi" Label=@Localizer["Kigurumi"]>
@foreach (var index in kigurumiUniqueIdList)
{
var costumeTitle = GameDataService.GetKigurumiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@response.Puchi" Label=@Localizer["Puchi"]>
@foreach (var index in puchiUniqueIdList)
{
var costumeTitle = GameDataService.GetPuchiTitle(index);
<MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true">
@ -311,7 +252,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 +290,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" />

View File

@ -135,16 +135,16 @@ 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 =
{
@ -169,14 +169,14 @@ public partial class Profile
private Dictionary<Difficulty, List<SongBestData>> songBestDataMap = new();
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];
@ -189,25 +189,17 @@ public partial class Profile
if (LoginService.IsLoggedIn && !LoginService.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));
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();
breadcrumbs.Add(new BreadcrumbItem(Localizer["Profile"], href: $"/Users/{Baid}/Profile", disabled: false));
InitializeAvailableCostumes();
InitializeAvailableTitles();
songresponse = await Client.GetFromJsonAsync<SongBestResponse>($"api/PlayData/{Baid}");
songresponse.ThrowIfNull();
@ -229,16 +221,86 @@ 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;
}
UpdateScores(response.AchievementDisplayDifficulty);
if (response != null) 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 (LoginService.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().Except(GameDataService.GetLockedTitlePlateIdList()).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 (LoginService.AllowFreeProfileEditing)
{
titleUniqueIdList = GameDataService.GetTitleUniqueIdList();
var titles = GameDataService.GetTitles();
// Lock titles in LockedTitlesList but not in UnlockedTitle
var lockedTitleUniqueIdList = GameDataService.GetLockedTitleUniqueIdList().Except(unlockedTitle).ToList();
// Lock titles with rarity not in titlePlateIdList and not in unlockedTitle
lockedTitleUniqueIdList.AddRange(titles.Where(title => !titlePlateIdList.Contains(title.TitleRarity) && !unlockedTitle.Contains(title.TitleId)).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()
{
isSavingOptions = true;
@ -246,7 +308,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,49 +322,47 @@ 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)
{
foreach (var value in values)
switch (value.BestScoreRank)
{
switch (value.BestScoreRank)
{
case ScoreRank.Dondaful:
scoresArray[0]++;
break;
case ScoreRank.Gold:
scoresArray[1]++;
break;
case ScoreRank.Sakura:
scoresArray[2]++;
break;
case ScoreRank.Purple:
scoresArray[3]++;
break;
case ScoreRank.White:
scoresArray[4]++;
break;
case ScoreRank.Bronze:
scoresArray[5]++;
break;
case ScoreRank.Silver:
scoresArray[6]++;
break;
}
switch (value.BestCrown)
{
case CrownType.Clear:
scoresArray[7]++;
break;
case CrownType.Gold:
scoresArray[8]++;
break;
case CrownType.Dondaful:
scoresArray[9]++;
break;
}
case ScoreRank.Dondaful:
scoresArray[0]++;
break;
case ScoreRank.Gold:
scoresArray[1]++;
break;
case ScoreRank.Sakura:
scoresArray[2]++;
break;
case ScoreRank.Purple:
scoresArray[3]++;
break;
case ScoreRank.White:
scoresArray[4]++;
break;
case ScoreRank.Bronze:
scoresArray[5]++;
break;
case ScoreRank.Silver:
scoresArray[6]++;
break;
}
switch (value.BestCrown)
{
case CrownType.Clear:
scoresArray[7]++;
break;
case CrownType.Gold:
scoresArray[8]++;
break;
case CrownType.Dondaful:
scoresArray[9]++;
break;
}
}
}
public static string CostumeOrDefault(string file, uint id, string defaultfile)

View File

@ -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" />
FullWidth="true" Required="@true" RequiredError=@Localizer["Access Code is required"]
Label=@Localizer["Access Code"] Variant="Variant.Outlined" Margin="Margin.Dense" />
@if (LoginService.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,7 +11,8 @@ public partial class Register
private MudTimePicker timePicker = new();
private DateTime? date = DateTime.Today;
private TimeSpan? time = new TimeSpan(00, 45, 00);
private string inviteCode = "";
private DashboardResponse? response;
protected override async Task OnInitializedAsync()
@ -25,51 +26,57 @@ public partial class Register
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 LoginService.Register(accessCode, inputDateTime, password, confirmPassword, response, Client, 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

@ -43,14 +43,14 @@ namespace TaikoWebUI.Pages
if (LoginService.IsLoggedIn && !LoginService.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(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(_songTitle, href: $"/Users/{Baid}/Songs/{SongId}", disabled: false));
}
}

View File

@ -10,7 +10,7 @@
@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)
@ -65,12 +65,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>())

View File

@ -37,14 +37,14 @@ public partial class SongList
if (LoginService.IsLoggedIn && !LoginService.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(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false));
}
private async Task OnFavoriteToggled(SongBestData data)

View File

@ -7,7 +7,7 @@
@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)
{

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;
@ -18,6 +16,23 @@ public class GameDataService : IGameDataService
private string[] headTitles = { };
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)
{
@ -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
@ -150,16 +176,46 @@ 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()
{
shougouData.ThrowIfNull("Shouldn't happen!");
titleFlagArraySize = (int)shougouData.ShougouEntries.Max(entry => entry.UniqueId) + 1;
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!");
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}";
@ -188,41 +244,31 @@ 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 +279,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 +291,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 +303,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 +315,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 +364,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,7 +1,9 @@
using System.Security.Cryptography;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Options;
using TaikoWebUI.Settings;
using Blazored.LocalStorage;
namespace TaikoWebUI.Services;
@ -15,9 +17,14 @@ public class LoginService
public bool RegisterWithLastPlayTime { get; }
public bool AllowUserDelete { get; }
public bool AllowFreeProfileEditing { get; }
public LoginService(IOptions<WebUiSettings> settings)
public bool IsLoggedIn { get; private set; }
private User LoggedInUser { get; set; } = new();
public bool IsAdmin { get; private set; }
private readonly ILocalStorageService localStorage;
public LoginService(IOptions<WebUiSettings> settings, ILocalStorageService localStorage)
{
this.localStorage = localStorage;
IsLoggedIn = false;
IsAdmin = false;
var webUiSettings = settings.Value;
@ -28,131 +35,158 @@ public class LoginService
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)
public async Task<int> Login(string inputAccessCode, string inputPassword, HttpClient client)
{
// strip spaces or dashes from card number
inputCardNum = inputCardNum.Replace(" ", "").Replace("-", "");
inputAccessCode = inputAccessCode.Replace(" ", "").Replace("-", "").Replace(":", "");
foreach (var user in response.Users.Where(user => user.AccessCodes.Contains(inputCardNum)))
var request = new LoginRequest
{
foreach (var userCredential in response.UserCredentials.Where(userCredential => userCredential.Baid == user.Baid))
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
{
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;
}
"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);
return await LoginWithAuthToken(authToken, client) == false ? 5 : 1;
}
return 3;
}
public async Task<int> Register(string inputCardNum, DateTime inputDateTime, string inputPassword, string inputConfirmPassword,
DashboardResponse response, HttpClient client)
public async Task<bool> LoginWithAuthToken(string authToken, HttpClient client)
{
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(authToken);
// Check whether token is expired
if (jwtSecurityToken.ValidTo < DateTime.UtcNow) return false;
var baid = jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Name).Value;
var isAdmin = jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Role).Value == "Admin";
var response = await client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
var user = response?.Users.FirstOrDefault(u => u.Baid == uint.Parse(baid));
if (user is null) return false;
IsLoggedIn = true;
IsAdmin = isAdmin;
LoggedInUser = user;
OnLoginStatusChanged();
return true;
}
public async Task<int> Register(string inputCardNum, DateTime inputDateTime, string inputPassword,
string inputConfirmPassword,
DashboardResponse response, HttpClient client, string inviteCode)
{
if (OnlyAdmin) return 0;
if (inputPassword != inputConfirmPassword) return 2;
// strip spaces or dashes from card number
inputCardNum = inputCardNum.Replace(" ", "").Replace("-", "");
inputCardNum = inputCardNum.Replace(" ", "").Replace("-", "").Replace(":", "");
foreach (var user in response.Users.Where(user => user.AccessCodes.Contains(inputCardNum)))
var request = new RegisterRequest
{
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;
}
}
AccessCode = inputCardNum,
Password = inputPassword,
RegisterWithLastPlayTime = RegisterWithLastPlayTime,
LastPlayDateTime = inputDateTime,
InviteCode = inviteCode
};
return 3;
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
};
}
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,
public async Task<int> ChangePassword(string inputAccessCode, 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)))
if (inputNewPassword != inputConfirmNewPassword) return 2;
var request = new ChangePasswordRequest
{
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;
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 void Logout()
public async Task Logout()
{
IsLoggedIn = false;
LoggedInUser = new User();
IsAdmin = false;
// Clear JWT token
await localStorage.RemoveItemAsync("authToken");
OnLoginStatusChanged();
}
@ -174,7 +208,7 @@ public class LoginService
{
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 (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
{

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

@ -13,59 +13,61 @@
<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="Blazored.LocalStorage" Version="4.5.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>
<ProjectReference Include="..\SharedProject\SharedProject.csproj" />
<ProjectReference Include="..\SharedProject\SharedProject.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\music_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\musicinfo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\wordlist.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\musicinfo.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\music_order.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\wordlist.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\music_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\musicinfo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\datatable\wordlist.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Pages\Pages\AccessCode.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\ChangePassword.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\DaniDojo.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dashboard.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\AccessCodeDeleteConfirmDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\ChooseTitleDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\UserDeleteConfirmDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\UserQrCodeDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\HighScores.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Profile.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Register.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Users.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\AccessCode.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\ChangePassword.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\DaniDojo.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dashboard.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\AccessCodeDeleteConfirmDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\ChooseTitleDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\UserDeleteConfirmDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\UserQrCodeDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\HighScores.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Profile.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Register.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Users.razor" />
</ItemGroup>
<ItemGroup>
@ -80,8 +82,8 @@
<CustomToolNamespace>LocalizationResource</CustomToolNamespace>
</EmbeddedResource>
<EmbeddedResource Update="Localization\LocalizationResource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>LocalizationResource.Designer.cs</LastGenOutput>
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>LocalizationResource.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Localization\LocalizationResource.ja.resx">
<SubType>Designer</SubType>
@ -89,20 +91,20 @@
<CustomToolNamespace>LocalizationResource</CustomToolNamespace>
</EmbeddedResource>
<EmbeddedResource Update="Localization\LocalizationResource.zh-Hant.resx">
<SubType>Designer</SubType>
<Generator>ResXFileCodeGenerator</Generator>
<CustomToolNamespace>LocalizationResource</CustomToolNamespace>
<SubType>Designer</SubType>
<Generator>ResXFileCodeGenerator</Generator>
<CustomToolNamespace>LocalizationResource</CustomToolNamespace>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Localization\LocalizationResource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>LocalizationResource.resx</DependentUpon>
</Compile>
<Compile Update="Localization\LocalizationResource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>LocalizationResource.resx</DependentUpon>
</Compile>
</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 class="loader-container">
<div class="linear-progress"></div>
<div class="loading-text">
Loading...
</div>
<div id="app">
<div class="loader-container">
<div class="linear-progress"></div>
<div class="loading-text">
Loading...
</div>
</div>
</div>
<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>
window.blazorCulture = {
get: () => localStorage['BlazorCulture'],
set: (value) => localStorage['BlazorCulture'] = value
};
</script>
<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>
window.blazorCulture = {
get: () => localStorage['BlazorCulture'],
set: (value) => localStorage['BlazorCulture'] = value
};
</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>