using Robovoice.Core.Voices; using System.ComponentModel; namespace Robovoice.App; internal sealed partial class VoiceManagerForm : Form { private readonly string _voicesDir; private readonly VoiceCatalogue _catalogue; private IReadOnlyList _allVoices = Array.Empty(); private HashSet _installedKeys = new(); private bool _catalogueLoaded; public string? SelectedVoiceKey { get; private set; } public VoiceManagerForm(string voicesDir, string? currentVoiceKey) { InitializeComponent(); _voicesDir = voicesDir; _catalogue = new VoiceCatalogue(); SelectedVoiceKey = currentVoiceKey; txtFilter.TextChanged += (_, _) => ApplyFilter(); btnDownload.Click += OnDownload; btnRemove.Click += OnRemove; btnClose.Click += (_, _) => Close(); gridVoices.SelectionChanged += OnSelectionChanged; Load += OnLoad; FormClosing += OnFormClosing; } private async void OnLoad(object? sender, EventArgs e) { btnDownload.Enabled = false; btnRemove.Enabled = false; RefreshInstalled(); lblStatus.Text = "Fetching voice catalogue..."; progressBar.Visible = true; progressBar.Style = ProgressBarStyle.Marquee; try { _allVoices = await _catalogue.GetCatalogueAsync(); _catalogueLoaded = true; PopulateGrid(); lblStatus.Text = $"{_allVoices.Count} voices available, {_installedKeys.Count} installed"; } catch (Exception ex) { lblStatus.Text = $"Failed to fetch catalogue: {ex.Message}"; } finally { progressBar.Visible = false; } } private void RefreshInstalled() { _installedKeys = VoiceCatalogue .GetInstalledVoices(_voicesDir) .ToHashSet(); } private void PopulateGrid() { gridVoices.Rows.Clear(); gridVoices.Columns.Clear(); gridVoices.Columns.Add(new DataGridViewTextBoxColumn { Name = "Key", HeaderText = "Key", FillWeight = 40, }); gridVoices.Columns.Add(new DataGridViewTextBoxColumn { Name = "Language", HeaderText = "Language", FillWeight = 25, }); gridVoices.Columns.Add(new DataGridViewTextBoxColumn { Name = "Voice", HeaderText = "Voice", FillWeight = 20, }); gridVoices.Columns.Add(new DataGridViewTextBoxColumn { Name = "Quality", HeaderText = "Quality", FillWeight = 15, }); gridVoices.Columns.Add(new DataGridViewTextBoxColumn { Name = "Size", HeaderText = "Size", FillWeight = 15, }); gridVoices.Columns.Add(new DataGridViewTextBoxColumn { Name = "Status", HeaderText = "Status", FillWeight = 15, }); foreach (var voice in _allVoices) { bool installed = _installedKeys.Contains(voice.Key); int rowIdx = gridVoices.Rows.Add( voice.Key, voice.Language.NameEnglish, voice.Name, voice.Quality, voice.SizeDisplay, installed ? "Installed" : "Available"); var row = gridVoices.Rows[rowIdx]; row.Tag = voice.Key; if (installed) { row.DefaultCellStyle.BackColor = Color.FromArgb(235, 245, 235); if (voice.Key == SelectedVoiceKey) { row.DefaultCellStyle.Font = new Font(gridVoices.Font, FontStyle.Bold); row.Selected = true; } } else { row.DefaultCellStyle.ForeColor = Color.Gray; } } if (gridVoices.SelectedRows.Count == 0 && gridVoices.Rows.Count > 0) gridVoices.Rows[0].Selected = true; } private void ApplyFilter() { string filter = txtFilter.Text.Trim().ToLowerInvariant(); if (string.IsNullOrEmpty(filter)) { foreach (DataGridViewRow row in gridVoices.Rows) row.Visible = true; return; } foreach (DataGridViewRow row in gridVoices.Rows) { if (row.Tag is not string key) { row.Visible = false; continue; } var voice = _allVoices.FirstOrDefault(v => v.Key == key); if (voice is null) { row.Visible = false; continue; } string haystack = $"{key} {voice.Name} {voice.Language.NameEnglish} {voice.Language.Code} {voice.Language.Family} {voice.Quality}".ToLowerInvariant(); row.Visible = haystack.Contains(filter); } } private void OnSelectionChanged(object? sender, EventArgs e) { if (gridVoices.SelectedRows.Count == 0) return; var row = gridVoices.SelectedRows[0]; if (row.Tag is not string key) return; bool installed = _installedKeys.Contains(key); btnDownload.Enabled = !installed && _catalogueLoaded; btnRemove.Enabled = installed; } private async void OnDownload(object? sender, EventArgs e) { if (gridVoices.SelectedRows.Count == 0) return; var row = gridVoices.SelectedRows[0]; if (row.Tag is not string key) return; var voice = _allVoices.FirstOrDefault(v => v.Key == key); if (voice is null) return; btnDownload.Enabled = false; btnRemove.Enabled = false; progressBar.Visible = true; progressBar.Style = ProgressBarStyle.Continuous; progressBar.Value = 0; lblStatus.Text = $"Downloading {key} ({voice.SizeDisplay})..."; try { var progress = new Progress<(long downloaded, long total)>(p => { if (p.total > 0) { progressBar.Value = (int)(p.downloaded * 100 / p.total); lblStatus.Text = $"Downloading {key}... {p.downloaded / (1024 * 1024)} / {p.total / (1024 * 1024)} MB"; } }); await _catalogue.DownloadVoiceAsync(voice, _voicesDir, progress); RefreshInstalled(); UpdateRowStatus(key, installed: true); lblStatus.Text = $"Downloaded {key} successfully."; } catch (Exception ex) { lblStatus.Text = $"Download failed: {ex.Message}"; } finally { progressBar.Visible = false; OnSelectionChanged(null, EventArgs.Empty); } } private void OnRemove(object? sender, EventArgs e) { if (gridVoices.SelectedRows.Count == 0) return; var row = gridVoices.SelectedRows[0]; if (row.Tag is not string key) return; var dlgResult = MessageBox.Show( $"Remove voice '{key}'?\nThis will delete the .onnx and .onnx.json files.", "Confirm Remove", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (dlgResult != DialogResult.Yes) return; try { VoiceCatalogue.RemoveVoice(_voicesDir, key); RefreshInstalled(); UpdateRowStatus(key, installed: false); if (SelectedVoiceKey == key) SelectedVoiceKey = null; lblStatus.Text = $"Removed {key}."; } catch (Exception ex) { lblStatus.Text = $"Remove failed: {ex.Message}"; } OnSelectionChanged(null, EventArgs.Empty); } private void UpdateRowStatus(string key, bool installed) { foreach (DataGridViewRow row in gridVoices.Rows) { if (row.Tag is string rowKey && rowKey == key) { row.Cells["Status"].Value = installed ? "Installed" : "Available"; row.DefaultCellStyle.BackColor = installed ? Color.FromArgb(235, 245, 235) : Color.White; row.DefaultCellStyle.ForeColor = installed ? Color.Black : Color.Gray; break; } } lblStatus.Text = $"{_allVoices.Count} voices available, {_installedKeys.Count} installed"; } private void OnFormClosing(object? sender, FormClosingEventArgs e) { if (gridVoices.SelectedRows.Count > 0 && gridVoices.SelectedRows[0].Tag is string key) { if (_installedKeys.Contains(key)) SelectedVoiceKey = key; } _catalogue.DisposeAsync().AsTask().Wait(); } }