@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
@*True if debug, false for not debug. (TODO: Use an extensíonMethod: https://stackoverflow.com/questions/4696175/preprocessor-directives-in-razor)*@
@if (false)
{
@if (!Answers.Any(x => x == null))
{
Avoid
}
}
@if (!Answers.Any(x => x == null))
{
if (useChagingFonts)
{
@GetDisplayChar()
}
else
{
@GetDisplayChar()
}
}
@if (!Answers.Any(x=>x == null))
{
@for (int i = 0; i < Answers.Length; i++)
{
int buttonIndex = i;
SelectButton(buttonIndex)"
Color="@(Answers[buttonIndex].isSelected ? Color.Primary : Color.Default)">
@Answers[buttonIndex].cchar.pinyin.ToTitleCase()
}
}
Meaning
Speech
@if (!Answers.Any(x => x == null))
{
Submit
}
@if (ignoreTone)
{
• Easy mode enabled
}
@if (useChagingFonts)
{
• Changing fonts enabled
}
@*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++)
{
i
}
}
@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? DontSkipTheseCChar;
private async Task 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("IgnoreTone");
}
if (await localStorage.ContainKeyAsync("ChangingFonts"))
{
useChagingFonts = await localStorage.GetItemAsync("ChangingFonts");
}
bool continueLast =
await localStorage.ContainKeyAsync("ContinueLearning")
&& await localStorage.ContainKeyAsync("ContinueData")
&& await localStorage.GetItemAsync("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("SelectedChunk");
if (!isSavedLocally)
Charecters = await httpClient.GetFromJsonAsync($"Data/Normalized_chunk_{selectedChunk.ToString("000")}.json");
else
{
string json = await localStorage.GetItemAsync($"Normalized_chunk_{selectedChunk.ToString("000")}.json");
Charecters = JsonConvert.DeserializeObject(json);
}
DontSkipTheseCChar = Charecters.Select(x => new CCharStats(x)).ToList();
}
async Task LoadCharectersFromContinue()
{
string continueDataJson = await localStorage.GetItemAsStringAsync("ContinueData");
CCharStats[] charectersLeft = JsonConvert.DeserializeObject(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($"Definition: {correctCChar.definition}", Severity.Success, config => { config.VisibleStateDuration = 1000; });
increaseCCharStat(GetCorrectCCharStats(), StatType.NumOfCorrects);
ChangeFont();
}
else
{
Snackbar.Add(
@
Incorrect!
- CChar: @correctCChar.charcter
- Correct answer: @correctCChar.pinyin.ToTitleCase()
- Meaning: @correctCChar.definition
, 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($"Definition: {GetCorrectCChar().definition}", Severity.Info);
}
public void ShowPinyin()
{
Snackbar.Add($"Pinyin: {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)(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!
}
}