Renamed project to CCharLearn
This commit is contained in:
parent
a6f55347b8
commit
591c3307a9
233 changed files with 16 additions and 17 deletions
17
CCharLearn/Pages/Counter.razor
Normal file
17
CCharLearn/Pages/Counter.razor
Normal file
|
@ -0,0 +1,17 @@
|
|||
@page "/counter"
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h3" GutterBottom="true">Counter</MudText>
|
||||
<MudText Class="mb-4">Current count: @currentCount</MudText>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" @onclick="IncrementCount">Click me</MudButton>
|
||||
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
52
CCharLearn/Pages/FetchData.razor
Normal file
52
CCharLearn/Pages/FetchData.razor
Normal file
|
@ -0,0 +1,52 @@
|
|||
@page "/fetchdata"
|
||||
@inject HttpClient Http
|
||||
@using CCharLearn;
|
||||
|
||||
<PageTitle>Weather forecast</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h3" GutterBottom="true">Weather forecast</MudText>
|
||||
<MudText Class="mb-8">This component demonstrates fetching data from the server.</MudText>
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Default" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="forecasts" Hover="true" SortLabel="Sort By" Elevation="0">
|
||||
<HeaderContent>
|
||||
<MudTh><MudTableSortLabel InitialDirection="SortDirection.Ascending" SortBy="new Func<WeatherForecast, object>(x=>x.Date)">Date</MudTableSortLabel></MudTh>
|
||||
<MudTh><MudTableSortLabel SortBy="new Func<WeatherForecast, object>(x=>x.TemperatureC)">Temp. (C)</MudTableSortLabel></MudTh>
|
||||
<MudTh><MudTableSortLabel SortBy="new Func<WeatherForecast, object>(x=>x.TemperatureF)">Temp. (F)</MudTableSortLabel></MudTh>
|
||||
<MudTh><MudTableSortLabel SortBy="new Func<WeatherForecast, object>(x=>x.Summary!)">Summary</MudTableSortLabel></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Date">@context.Date</MudTd>
|
||||
<MudTd DataLabel="Temp. (C)">@context.TemperatureC</MudTd>
|
||||
<MudTd DataLabel="Temp. (F)">@context.TemperatureF</MudTd>
|
||||
<MudTd DataLabel="Summary">@context.Summary</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager PageSizeOptions="new int[]{50, 100}" />
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
}
|
||||
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
|
||||
}
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public string? Summary { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
145
CCharLearn/Pages/Index.razor
Normal file
145
CCharLearn/Pages/Index.razor
Normal file
|
@ -0,0 +1,145 @@
|
|||
@page "/"
|
||||
@inject NavigationManager navigator
|
||||
@inject HttpClient httpClient
|
||||
@inject Blazored.LocalStorage.ILocalStorageService localStorage
|
||||
@inject SpeechSynthesis SpeechSynthesis
|
||||
|
||||
<PageTitle>Index</PageTitle>
|
||||
|
||||
<MudContainer Class="align-center justify-center d-flex">
|
||||
<MudContainer Style="width: 400px">
|
||||
<MudSelect Class="pt-12" @bind-Value=selectedChunk TextChanged="SelectedChunk" select T=int Label="Chunk" Variant="Variant.Filled" AnchorOrigin="Origin.BottomCenter">
|
||||
@for (int i = 0; i < numOfChunks; i++)
|
||||
{
|
||||
<MudSelectItem T="int" Value="@i" />
|
||||
}
|
||||
</MudSelect>
|
||||
<MudContainer Class="justify-center d-flex pb-16 pt-4">
|
||||
<MudButton Variant="Variant.Filled" OnClick=StartLearning Color="Color.Primary">Start learning!</MudButton>
|
||||
</MudContainer>
|
||||
|
||||
<MudContainer Class="justify-center d-flex">
|
||||
@if (Charecters != null)
|
||||
{
|
||||
<MudPaper Class="overflow-scroll rounded-0" Style="height: 50vh;" Elevation="1">
|
||||
<MudDataGrid FixedHeader=true Items="@Charecters">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.charcter" Title="CChar" />
|
||||
<PropertyColumn Property="x => x.pinyin" Title="Pinyin" />
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
<MudContainer Class="justify-center d-flex pt-4">
|
||||
<MudButton Class="mt-1 mb-7" Variant="Variant.Filled" Color="Color.Secondary" Disabled="@(isLoading || isSavedLocally)" OnClick="LoadAllChunksIntoLocalStorage">
|
||||
@if (!isSavedLocally)
|
||||
{
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudText>@($"{savedChunks}/{numOfChunks}")</MudText>
|
||||
<MudProgressCircular Class="pl-8 ms-n1 mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>Load chunks to local storage</MudText>
|
||||
<MudIcon Icon="@Icons.Material.Rounded.Send" Class="mr-1" />
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>Chunks are saved locally</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
</MudContainer>
|
||||
<MudContainer Class="justify-center d-flex pt-4">
|
||||
<MudButton OnClick="async ()=>{await localStorage.ClearAsync(); navigator.NavigateTo(string.Empty, true);}" Variant="Variant.Outlined" Color="Color.Tertiary">Delete local storage</MudButton>
|
||||
</MudContainer>
|
||||
</MudContainer>
|
||||
</MudContainer>
|
||||
|
||||
@code{
|
||||
int numOfChunks = 197; //Exclusive index 0
|
||||
int selectedChunk = 0;
|
||||
int savedChunks = 0;
|
||||
|
||||
bool isLoading = false;
|
||||
bool isSavedLocally = false;
|
||||
|
||||
protected async override Task OnInitializedAsync()
|
||||
{
|
||||
if (await localStorage.ContainKeyAsync("Normalized_chunk_001.json"))
|
||||
isSavedLocally = true;
|
||||
|
||||
SelectedChunk();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
async void StartLearning()
|
||||
{
|
||||
await localStorage.SetItemAsync("SelectedChunk", selectedChunk);
|
||||
navigator.NavigateTo("/Learn");
|
||||
}
|
||||
|
||||
CChar[]? Charecters;
|
||||
async void SelectedChunk()
|
||||
{
|
||||
if (await localStorage.ContainKeyAsync("Normalized_chunk_001.json"))
|
||||
isSavedLocally = true;
|
||||
|
||||
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);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
async Task LoadAllChunksIntoLocalStorage()
|
||||
{
|
||||
isLoading = true;
|
||||
string[] jsonChunks = new string[numOfChunks];
|
||||
|
||||
Parallel.For(0, numOfChunks, async (i)=>
|
||||
{
|
||||
await Task.Delay(10*i);
|
||||
try
|
||||
{
|
||||
jsonChunks[i] = await httpClient.GetStringAsync($"Data/Normalized_chunk_{i.ToString("000")}.json");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
jsonChunks[i] = await httpClient.GetStringAsync($"Data/Normalized_chunk_{i.ToString("000")}.json");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(jsonChunks[i])) throw new Exception("Couldn't load chunk: " + i);
|
||||
|
||||
try
|
||||
{
|
||||
await localStorage.SetItemAsync($"Normalized_chunk_{i.ToString("000")}.json", jsonChunks[i]);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
await localStorage.SetItemAsync($"Normalized_chunk_{i.ToString("000")}.json", jsonChunks[i]);
|
||||
}
|
||||
|
||||
if (!await localStorage.ContainKeyAsync($"Normalized_chunk_{i.ToString("000")}.json")) throw new Exception("Couldn't save chunk: " + i);
|
||||
});
|
||||
|
||||
while (jsonChunks.Any(x => x == null))
|
||||
{
|
||||
await Task.Delay(1);
|
||||
savedChunks = jsonChunks.Count(x=>x != null);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
isSavedLocally = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
340
CCharLearn/Pages/Learn.razor
Normal file
340
CCharLearn/Pages/Learn.razor
Normal file
|
@ -0,0 +1,340 @@
|
|||
@page "/Learn"
|
||||
@using System.Text;
|
||||
@using CCharLearn.ExtensionMethods;
|
||||
|
||||
@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" Style="height: 100%;">
|
||||
<MudStack Class="d-flex">
|
||||
<MudStack Row=true Class="pt-2 d-flex justify-center">
|
||||
@if (!Answers.Any(x => x == null))
|
||||
{
|
||||
@*Color="@(selectedCorrect ? Color.Success : Color.Error)"*@
|
||||
<MudButton Disabled="@(!(Answers.Any(x=>x.isSelected)))" OnClick="()=>RemoveCCharFromSelection()" Variant="Variant.Outlined">Avoid</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
<MudContainer>
|
||||
<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))
|
||||
{
|
||||
<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">
|
||||
<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">
|
||||
@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>
|
||||
}
|
||||
</MudContainer>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
bool isSavedLocally = false;
|
||||
|
||||
bool selectedCorrect = 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;
|
||||
|
||||
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();
|
||||
Program.CCharsLeft = DontSkipTheseCChar.Count;
|
||||
Program.InvokeUiUpdate();
|
||||
|
||||
GenerateQuestion();
|
||||
}
|
||||
|
||||
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].cchar.pinyin += "_";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
navigator.NavigateTo("");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
GenerateQuestion();
|
||||
}
|
||||
|
||||
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 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();
|
||||
GenerateQuestion();
|
||||
}
|
||||
|
||||
public void ShowMeaning()
|
||||
{
|
||||
Snackbar.Add($"<b>Definition:</b> {GetCorrectCChar().definition}", Severity.Info);
|
||||
}
|
||||
|
||||
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"));
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
async Task SayCorrectChar()
|
||||
{
|
||||
if (SpeechVoice == null)
|
||||
{
|
||||
Snackbar.Add("Couldn't play sound", 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!
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue