459 lines
15 KiB
Text
459 lines
15 KiB
Text
@page "/Learn"
|
|
@using System.Text;
|
|
@using CCharLearn.ExtensionMethods;
|
|
@using UnidecodeSharpFork;
|
|
|
|
@inject NavigationManager navigator
|
|
@inject Blazored.LocalStorage.ILocalStorageService localStorage
|
|
@inject HttpClient httpClient
|
|
@inject ISnackbar Snackbar
|
|
@inject SpeechSynthesis SpeechSynthesis
|
|
|
|
<style>
|
|
.LargeCharecter{
|
|
width: 0;
|
|
height: 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-size: 125px;
|
|
}
|
|
|
|
.PinyinButtons{
|
|
width: 125px;
|
|
font-size: 20px;
|
|
}
|
|
</style>
|
|
|
|
<MudContainer Class="justify-center d-flex ma-0 overflow-hidden" Style="height: 100%;">
|
|
<MudStack Class="d-flex">
|
|
@*True if debug, false for not debug. (TODO: Use an extensíonMethod: https://stackoverflow.com/questions/4696175/preprocessor-directives-in-razor)*@
|
|
@if (false)
|
|
{
|
|
<MudStack Row=true Class="pt-2 d-flex justify-center">
|
|
@if (!Answers.Any(x => x == null))
|
|
{
|
|
<MudButton Disabled="@(!(Answers.Any(x=>x.isSelected)))" OnClick="()=>RemoveCCharFromSelection()" Variant="Variant.Outlined">Avoid</MudButton>
|
|
}
|
|
</MudStack>
|
|
}
|
|
<MudContainer Class="pt-12">
|
|
<MudPaper Class="pa-16 ma-2 rounded-xl mud-dark" Elevation="1">
|
|
<MudContainer Style="width: 100px; height: 100px" Class="pa-8 ma-4 d-flex justify-center align-center">
|
|
@if (!Answers.Any(x => x == null))
|
|
{
|
|
if (useChagingFonts)
|
|
{
|
|
<p class="LargeCharecter" style="font-family: '@currentFont';">@GetDisplayChar()</p>
|
|
}
|
|
else
|
|
{
|
|
<p class="LargeCharecter">@GetDisplayChar()</p>
|
|
}
|
|
}
|
|
</MudContainer>
|
|
</MudPaper>
|
|
</MudContainer>
|
|
<MudContainer Class="pa-8 pt-10 justify-center align-center d-flex"
|
|
Style="max-width:300px">
|
|
<MudGrid Spacing="10" Class="align-center justify-center d-flex">
|
|
@if (!Answers.Any(x=>x == null))
|
|
{
|
|
@for (int i = 0; i < Answers.Length; i++)
|
|
{
|
|
int buttonIndex = i;
|
|
<MudButton Class="PinyinButtons ma-3"
|
|
Variant="Variant.Outlined"
|
|
@onclick="() => SelectButton(buttonIndex)"
|
|
Color="@(Answers[buttonIndex].isSelected ? Color.Primary : Color.Default)">
|
|
<MudText>
|
|
@Answers[buttonIndex].cchar.pinyin.ToTitleCase()
|
|
</MudText>
|
|
</MudButton>
|
|
}
|
|
}
|
|
</MudGrid>
|
|
</MudContainer>
|
|
<MudSpacer/>
|
|
<MudContainer Class="justify-center d-flex py-4">
|
|
<MudStack Row=true>
|
|
<MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="ShowMeaning">Meaning</MudButton>
|
|
<MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="SayCorrectChar">Speech</MudButton>
|
|
</MudStack>
|
|
</MudContainer>
|
|
<MudContainer Class="pb-8 justify-center align-center d-flex">
|
|
<MudStack Spacing="0">
|
|
@if (!Answers.Any(x => x == null))
|
|
{
|
|
<MudButton Disabled="@(!(Answers.Any(x=>x.isSelected)))" OnClick="Submit" Class="px-8 py-3" Variant="Variant.Filled" Size="Size.Large" Color="Color.Success" Style="font-size: 20px;"> Submit</MudButton>
|
|
}
|
|
@if (ignoreTone)
|
|
{
|
|
<MudText Typo="Typo.caption">• Easy mode enabled</MudText>
|
|
}
|
|
@if (useChagingFonts)
|
|
{
|
|
<MudText Typo="Typo.caption">• Changing fonts enabled</MudText>
|
|
}
|
|
</MudStack>
|
|
</MudContainer>
|
|
</MudStack>
|
|
</MudContainer>
|
|
|
|
@*Used to load all fonts into memory instead of loading from http each time*@
|
|
@if (useChagingFonts && !hasLoadedFontsToMemory)
|
|
{
|
|
@for (int i = 0; i < extraFonts.Length; i++)
|
|
{
|
|
<p style="font-family: '@extraFonts[i]';">i</p>
|
|
}
|
|
}
|
|
|
|
@code {
|
|
bool isSavedLocally = false;
|
|
bool ignoreTone = false;
|
|
|
|
bool selectedCorrect = false;
|
|
|
|
bool useChagingFonts = false;
|
|
string? currentFont = null;
|
|
string[] extraFonts = { "HanyiSentyRubber", "mini-jian-caocuyuan", "myoungheihk", "UnboundedSans", "wt064", "XiaolaiSC-Regular", "chinese1", "chinese2" };
|
|
bool hasLoadedFontsToMemory = false;
|
|
|
|
public Answer[] Answers = new Answer[4];
|
|
public void SelectButton(int selectedIndex)
|
|
{
|
|
for (int i = 0; i < Answers.Length; i++)
|
|
{
|
|
Answers[i].isSelected = (i == selectedIndex);
|
|
}
|
|
|
|
selectedCorrect = Answers[selectedIndex].isCorrect;
|
|
}
|
|
|
|
private CChar[]? _charecters;
|
|
public CChar[]? Charecters
|
|
{
|
|
get
|
|
{
|
|
if (_charecters == null) throw new Exception("Loaded dataset empty?");
|
|
|
|
return _charecters;
|
|
}
|
|
set { _charecters = value; }
|
|
}
|
|
|
|
public List<CCharStats>? DontSkipTheseCChar;
|
|
|
|
private async Task<string> GetFileContentsAsync(string filePath)
|
|
{
|
|
var fileResponse = await httpClient.GetByteArrayAsync(filePath);
|
|
return Encoding.UTF8.GetString(fileResponse);
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
Program.UpdateUiEvent += OnUiUpdate;
|
|
if (await localStorage.ContainKeyAsync("IgnoreTone"))
|
|
{
|
|
ignoreTone = await localStorage.GetItemAsync<bool>("IgnoreTone");
|
|
}
|
|
|
|
if (await localStorage.ContainKeyAsync("ChangingFonts"))
|
|
{
|
|
useChagingFonts = await localStorage.GetItemAsync<bool>("ChangingFonts");
|
|
}
|
|
|
|
bool continueLast =
|
|
await localStorage.ContainKeyAsync("ContinueLearning")
|
|
&& await localStorage.ContainKeyAsync("ContinueData")
|
|
&& await localStorage.GetItemAsync<bool>("ContinueLearning");
|
|
|
|
if (continueLast)
|
|
{
|
|
await LoadCharectersFromContinue();
|
|
}
|
|
else
|
|
{
|
|
await LoadCharectersFromChunk();
|
|
}
|
|
|
|
if (ignoreTone)
|
|
DontSkipTheseCChar.ForEach(x => x.cchar.pinyin = x.cchar.pinyin.Unidecode());
|
|
|
|
if (useChagingFonts)
|
|
{
|
|
ChangeFont();
|
|
}
|
|
|
|
Program.CCharsLeft = DontSkipTheseCChar.Count;
|
|
Program.InvokeUiUpdate();
|
|
|
|
GenerateQuestion();
|
|
|
|
await Task.Delay(100);
|
|
hasLoadedFontsToMemory = true;
|
|
}
|
|
|
|
async Task LoadCharectersFromChunk()
|
|
{
|
|
isSavedLocally = await localStorage.ContainKeyAsync("Normalized_chunk_001.json");
|
|
|
|
int selectedChunk = await localStorage.GetItemAsync<int>("SelectedChunk");
|
|
|
|
if (!isSavedLocally)
|
|
Charecters = await httpClient.GetFromJsonAsync<CChar[]>($"Data/Normalized_chunk_{selectedChunk.ToString("000")}.json");
|
|
else
|
|
{
|
|
string json = await localStorage.GetItemAsync<string>($"Normalized_chunk_{selectedChunk.ToString("000")}.json");
|
|
Charecters = JsonConvert.DeserializeObject<CChar[]>(json);
|
|
}
|
|
|
|
DontSkipTheseCChar = Charecters.Select(x => new CCharStats(x)).ToList();
|
|
}
|
|
|
|
async Task LoadCharectersFromContinue()
|
|
{
|
|
string continueDataJson = await localStorage.GetItemAsStringAsync("ContinueData");
|
|
CCharStats[] charectersLeft = JsonConvert.DeserializeObject<CCharStats[]>(continueDataJson);
|
|
|
|
DontSkipTheseCChar = charectersLeft.ToList();
|
|
}
|
|
|
|
void OnUiUpdate() => StateHasChanged();
|
|
|
|
void GenerateQuestion()
|
|
{
|
|
if (DontSkipTheseCChar.Count < 5)
|
|
{
|
|
FinishAndKickBackToLearn();
|
|
return;
|
|
}
|
|
|
|
int correctIndex = Random.Shared.Next(0, Answers.Length);
|
|
|
|
for (int i = 0; i < Answers.Length; i++)
|
|
{
|
|
bool isCorrect = i == correctIndex;
|
|
CChar randomCChar;
|
|
|
|
repickRandomCChar:
|
|
randomCChar = DontSkipTheseCChar[Random.Shared.Next(0, DontSkipTheseCChar.Count)].cchar;
|
|
if (Answers.Any(x =>x != null && x.cchar == randomCChar)) goto repickRandomCChar;
|
|
|
|
Answers[i] = new Answer(randomCChar, isCorrect);
|
|
}
|
|
|
|
string correctPinyin = GetCorrectCChar().pinyin;
|
|
|
|
for (int i = 0; i < Answers.Length; i++)
|
|
{
|
|
if (Answers[i].isCorrect) continue;
|
|
if (Answers[i].cchar.pinyin != correctPinyin) continue;
|
|
|
|
Answers[i].isCorrect = true;
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
async void FinishAndKickBackToLearn()
|
|
{
|
|
Snackbar.Add("Congrats, you have compleated this chunk!", Severity.Success, config =>
|
|
{
|
|
config.RequireInteraction = true;
|
|
config.CloseAfterNavigation = false;
|
|
});
|
|
Program.UpdateUiEvent -= OnUiUpdate;
|
|
Program.CCharsLeft = 0;
|
|
|
|
await localStorage.RemoveItemAsync("ContinueData");
|
|
|
|
navigator.NavigateTo("");
|
|
}
|
|
|
|
async void ChangeFont()
|
|
{
|
|
int randomIndex = Random.Shared.Next(0, extraFonts.Length);
|
|
currentFont = extraFonts[randomIndex];
|
|
}
|
|
|
|
async void Submit()
|
|
{
|
|
bool isCorrect = Answers.Any(x => x.isCorrect && x.isSelected);
|
|
CCharStats correctCCharStats = GetCorrectCCharStats();
|
|
CChar correctCChar = correctCCharStats.cchar;
|
|
|
|
if (isCorrect)
|
|
{
|
|
Snackbar.Add($"<b>Definition:</b> {correctCChar.definition}", Severity.Success, config => { config.VisibleStateDuration = 1000; });
|
|
increaseCCharStat(GetCorrectCCharStats(), StatType.NumOfCorrects);
|
|
ChangeFont();
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add(
|
|
@<div>
|
|
<h3>Incorrect!</h3>
|
|
<ul>
|
|
<li>CChar: @correctCChar.charcter</li>
|
|
<li>Correct answer: @correctCChar.pinyin.ToTitleCase()</li>
|
|
<li>Meaning: @correctCChar.definition</li>
|
|
</ul>
|
|
</div>
|
|
, Severity.Error);
|
|
increaseCCharStat(GetCorrectCCharStats(), StatType.NumOfWrongs);
|
|
}
|
|
|
|
if (correctCCharStats.TotalAnswers >= 3)
|
|
{
|
|
if (correctCCharStats.Accuracy > 0.7f)
|
|
{
|
|
RemoveCCharFromSelection(true);
|
|
|
|
Snackbar.Add($"CChar '{correctCChar.charcter}' compleated!", Severity.Success);
|
|
}
|
|
}
|
|
|
|
await SaveContinueData();
|
|
|
|
if (isCorrect)
|
|
{
|
|
GenerateQuestion();
|
|
}
|
|
else
|
|
{
|
|
foreach (var anwser in Answers)
|
|
{
|
|
anwser.isSelected = false;
|
|
}
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
async Task SaveContinueData()
|
|
{
|
|
CCharStats[] charectersLeft = DontSkipTheseCChar.ToArray();
|
|
string continueDataJson = JsonConvert.SerializeObject(charectersLeft);
|
|
|
|
await localStorage.SetItemAsStringAsync("ContinueData", continueDataJson);
|
|
await localStorage.SetItemAsync("ContinueCharectersLeft", charectersLeft.Length);
|
|
}
|
|
|
|
public class Answer
|
|
{
|
|
public Answer()
|
|
{
|
|
}
|
|
|
|
public Answer(CChar cchar, bool isCorrect)
|
|
{
|
|
this.cchar = cchar;
|
|
this.isCorrect = isCorrect;
|
|
}
|
|
|
|
public CChar cchar { get; set; }
|
|
public bool isCorrect { get; set; } = false;
|
|
public bool isSelected { get; set; } = false;
|
|
}
|
|
|
|
public char GetDisplayChar()
|
|
{
|
|
char? cc = GetCorrectCChar().charcter;
|
|
if (cc == null) return ' ';
|
|
else return (char)cc;
|
|
}
|
|
|
|
public CChar GetCorrectCChar()
|
|
{
|
|
CChar cchar = Answers.FirstOrDefault(x => x.isCorrect)?.cchar;
|
|
return cchar;
|
|
}
|
|
|
|
public CCharStats GetCorrectCCharStats()
|
|
{
|
|
CChar correctChar = GetCorrectCChar();
|
|
CCharStats correctCCharStats = DontSkipTheseCChar.First(x => x.cchar == correctChar);
|
|
return correctCCharStats;
|
|
}
|
|
|
|
public void increaseCCharStat(CCharStats stats, StatType statType)
|
|
{
|
|
int correctStatsIndex = DontSkipTheseCChar.IndexOf(stats);
|
|
switch (statType)
|
|
{
|
|
case StatType.NumOfCorrects:
|
|
DontSkipTheseCChar[correctStatsIndex].NumOfCorrects++;
|
|
break;
|
|
|
|
case StatType.NumOfWrongs:
|
|
DontSkipTheseCChar[correctStatsIndex].NumOfWrongs++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
public async void RemoveCCharFromSelection(bool ignoreSelection = false)
|
|
{
|
|
CCharStats correctCCharStats = GetCorrectCCharStats();
|
|
|
|
|
|
if (!selectedCorrect && !ignoreSelection)
|
|
{
|
|
Snackbar.Add("Selected is wrong. Try again!", Severity.Error);
|
|
return;
|
|
}
|
|
else if (!ignoreSelection)// Hacky way to only display this snackbar when is manually removed
|
|
{
|
|
Snackbar.Add($"Removed '{correctCCharStats.cchar.charcter}' from selection", Severity.Info, config => config.VisibleStateDuration = 3000);
|
|
}
|
|
|
|
DontSkipTheseCChar.Remove(correctCCharStats);
|
|
Console.WriteLine("Remaining CChars: " + DontSkipTheseCChar);
|
|
Program.CCharsLeft = DontSkipTheseCChar.Count;
|
|
Program.InvokeUiUpdate();
|
|
|
|
await SaveContinueData();
|
|
|
|
GenerateQuestion();
|
|
}
|
|
|
|
public void ShowMeaning()
|
|
{
|
|
Snackbar.Add($"<b>Definition:</b> {GetCorrectCChar().definition}", Severity.Info, conf => conf.VisibleStateDuration = 1000);
|
|
}
|
|
|
|
public void ShowPinyin()
|
|
{
|
|
Snackbar.Add($"<b>Pinyin:</b> {GetCorrectCChar().pinyin.ToTitleCase()}", Severity.Info);
|
|
}
|
|
|
|
SpeechSynthesisVoice? SpeechVoice;
|
|
|
|
protected async override Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
// Gets chinese voice on windows or iphone.
|
|
this.SpeechVoice = ((IEnumerable<SpeechSynthesisVoice>)(await this.SpeechSynthesis.GetVoicesAsync())).FirstOrDefault(v => v.Name.Contains("Yaoyao") || v.Name.Contains("Ting-Ting") || v.Name.ToLower().Contains("cn"));
|
|
this.StateHasChanged();
|
|
}
|
|
}
|
|
|
|
async Task SayCorrectChar()
|
|
{
|
|
if (SpeechVoice == null)
|
|
{
|
|
Snackbar.Add("Couldn't play sound.\nLikely reason: Chinese speech not installed", Severity.Error);
|
|
return;
|
|
}
|
|
|
|
var utterancet = new SpeechSynthesisUtterance
|
|
{
|
|
Text = this.GetCorrectCChar().charcter.ToString(),
|
|
Voice = this.SpeechVoice,
|
|
Rate = 0.75f
|
|
};
|
|
|
|
await this.SpeechSynthesis.SpeakAsync(utterancet); // 👈 Speak with "Haruka"'s voice!
|
|
}
|
|
}
|