@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; @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, conf => conf.VisibleStateDuration = 1000); } 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! } }