namespace PainoBar_Helper { public partial class StationForm : Form { private readonly PianobarManager _pianobarManager; private List _stations; private List _filteredStations; private bool _stationsLoaded = false; private bool _stationEntriesStarted = false; private sealed class StationItem { public int Index { get; set; } public string Name { get; set; } = string.Empty; public override string ToString() => Name; } public StationForm(PianobarManager pianobarManager) { InitializeComponent(); _pianobarManager = pianobarManager; _stations = new List(); _filteredStations = new List(); InitializeControls(); } #region Initialization private void InitializeControls() { // Uses textBox1 (search box) and lstStations from the Designer // Uses btnSelectStation from the Designer if (lstStations != null) { lstStations.DoubleClick += LstStations_DoubleClick; } if (refresh_button != null) { refresh_button.Click += refresh_button_Click; } } protected override void OnShown(EventArgs e) { base.OnShown(e); if (!_stationsLoaded) LoadStations(); } private void LoadStations(bool forceRefresh = false) { _filteredStations.Clear(); _stationsLoaded = false; _stationEntriesStarted = false; if (forceRefresh) _stations.Clear(); var knownStations = _pianobarManager.GetKnownStationEntries(); if (knownStations.Count > 0) { _stations = knownStations .Select(s => new StationItem { Index = s.Index, Name = s.Name }) .OrderBy(s => s.Index) .ToList(); _stationEntriesStarted = true; if (lstStations != null) UpdateStationList(); } _pianobarManager.OutputReceived -= PianobarManager_OutputReceived; _pianobarManager.OutputReceived += PianobarManager_OutputReceived; // Only request station list on explicit refresh. if (forceRefresh) { try { _pianobarManager.ShowStations(); if (lstStations != null && _stations.Count == 0) { lstStations.Items.Clear(); lstStations.Items.Add("Loading stations..."); } } catch (Exception ex) { MessageBox.Show($"Failed to load stations:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } else { _stationsLoaded = true; if (_stations.Count == 0 && lstStations != null) { lstStations.Items.Clear(); lstStations.Items.Add("No cached stations. Click Refresh stations."); } } } #endregion #region Station Loading private void PianobarManager_OutputReceived(object? sender, string e) { var line = StripAnsiCodes(e).Trim(); if (string.IsNullOrEmpty(line)) return; var match = System.Text.RegularExpressions.Regex.Match(line, @"^\s*(\d+)\)\s*(?:[qQ]\s+)?(.+?)\s*$"); if (match.Success) { _stationEntriesStarted = true; int stationIndex = int.Parse(match.Groups[1].Value); string stationName = match.Groups[2].Value.Trim(); var existing = _stations.FirstOrDefault(s => s.Index == stationIndex); if (existing == null) { _stations.Add(new StationItem { Index = stationIndex, Name = stationName }); _stations = _stations.OrderBy(s => s.Index).ToList(); if (!IsDisposed && IsHandleCreated) BeginInvoke(() => UpdateStationList()); } else if (!string.Equals(existing.Name, stationName, StringComparison.Ordinal)) { existing.Name = stationName; if (!IsDisposed && IsHandleCreated) BeginInvoke(() => UpdateStationList()); } return; } // Detect end of station list only after we started receiving entries if (_stationEntriesStarted && (line.Contains("Select station:", StringComparison.OrdinalIgnoreCase) || line == "[?]")) { _stationsLoaded = true; _pianobarManager.OutputReceived -= PianobarManager_OutputReceived; if (!IsDisposed && IsHandleCreated) BeginInvoke(() => FinalizeStationList()); } } private static string StripAnsiCodes(string value) { return System.Text.RegularExpressions.Regex.Replace(value, @"\x1B\[[0-9;]*[A-Za-z]", string.Empty); } private void UpdateStationList() { if (lstStations == null) return; lstStations.Items.Clear(); foreach (var station in _filteredStations.Any() ? _filteredStations : _stations) { lstStations.Items.Add(station); } } private void FinalizeStationList() { if (lstStations != null) { if (_stations.Count == 0) { lstStations.Items.Clear(); lstStations.Items.Add("No stations available"); } } } #endregion #region Search // Wire this to textBox1.TextChanged in the Designer public void textBox1_TextChanged(object? sender, EventArgs e) { if (sender is not TextBox txtSearch) return; string searchText = txtSearch.Text.Trim(); if (string.IsNullOrWhiteSpace(searchText)) { _filteredStations.Clear(); } else { _filteredStations = _stations .Where(s => s.Name.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0) .ToList(); } UpdateStationList(); } #endregion #region Station Selection // Wire this to btnSelectStation.Click in the Designer public async void btnSelectStation_Click(object? sender, EventArgs e) { await SelectCurrentStationAsync(); } private async void LstStations_DoubleClick(object? sender, EventArgs e) { await SelectCurrentStationAsync(); } private async Task SelectCurrentStationAsync() { if (lstStations == null) return; if (lstStations.SelectedIndex < 0) { MessageBox.Show("Please select a station.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (lstStations.SelectedItem is not StationItem selectedStation) return; try { await _pianobarManager.SelectStationAsync(selectedStation.Index); DialogResult = DialogResult.OK; Close(); } catch (Exception ex) { MessageBox.Show($"Failed to select station:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } public void refresh_button_Click(object? sender, EventArgs e) { LoadStations(forceRefresh: true); } #endregion #region Cancel // Wire this to btnCancelStation.Click in the Designer (or Cancel button) public void btnCancelStation_Click(object? sender, EventArgs e) { DialogResult = DialogResult.Cancel; Close(); } #endregion #region Cleanup protected override void OnFormClosing(FormClosingEventArgs e) { _pianobarManager.OutputReceived -= PianobarManager_OutputReceived; base.OnFormClosing(e); } #endregion } }