diff --git a/IMPLEMENTATION_GUIDE.md b/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..a383066 --- /dev/null +++ b/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,261 @@ +# Pianobar Helper - Implementation Guide + +## Overview +This document provides complete implementation details for the Pianobar Helper Windows Forms application. + +## Architecture + +### Core Components Created + +1. **ProfileManager.cs** - Backend profile management system + - Handles profile creation, deletion, and configuration + - Stores profiles in JSON format in AppData + - Generates Pianobar config files with credentials + +2. **PianobarManager.cs** - Process lifecycle manager + - Direct `pianobar.exe` process control + - Real-time stdout/stdin communication + - Song metadata parsing + - Event-driven architecture + +3. **OutputManager.cs** - Multi-output mechanism handler + - Text file export + - Local web server (JSON endpoint) + - Windows Toast Notifications (placeholder) + - Windows SMTC integration (placeholder) + +4. **MainForm (Main.cs)** - Primary application interface +5. **SettingsForm (Settings.cs)** - Profile and configuration management +6. **StationForm (StationForm.cs)** - Station picker interface + +## Designer File Issues + +The Designer files (.Designer.cs) need to be regenerated through Visual Studio's Form Designer. Here's what needs to be done: + +### MainForm Designer Controls Required + +Open `Main.cs` in the Form Designer and add these controls: + +**MenuStrip (menuStrip1):** +- `fileToolStripMenuItem` - "File" menu + - `settingsToolStripMenuItem` - "Settings..." menu item + - `profilesToolStripMenuItem` - "Profiles" submenu (dynamically populated) + +**StatusStrip (statusStrip1):** +- `statusLabelLeft` (ToolStripStatusLabel) - Connection indicator +- `statusLabelCenter` (ToolStripStatusLabel) - Alternating profile/station info +- `statusLabelRight` (ToolStripStatusLabel) - Event feed + +**Buttons:** +- `connect_button` - "Connect" button (wire to `btnConnect_Click`) +- `disconnect_button` - "Disconnect" button (wire to `btnDisconnect_Click`) +- `stations_button` - "Stations" button (wire to `btnStations_Click`) + +**GroupBox (groupBox1) - "Playback Controls":** +- `btnPlayPause` - "Play / Pause" button (wire to `btnPlayPause_Click`) +- `btnSkip` - "Skip" button (wire to `btnSkip_Click`) + +**Additional Controls:** +- `album_art_picture_box` (PictureBox) - For album art display +- `info_table_Panel` (TableLayoutPanel) - For song metadata display + +### SettingsForm Designer Controls Required + +Open `Settings.cs` in the Form Designer and add these controls: + +**GroupBox (profiles_group_box) - "Profile Management":** +- `selected_profile_label` - Label: "Selected profile:" +- `profiles_combo_box_selector` (ComboBox) - Profile selector (wire to `profiles_combo_box_selector_SelectedIndexChanged`) +- `new_profile_button` - "+ New" button (wire to `new_profile_button_Click`) +- `delete_profile_button` - "Delete" button (wire to `delete_profile_button_Click`) + +**GroupBox (profile_configuration_group_box) - "Profile Configuration":** + +**TabControl (profile_config_tab_control):** + +**Tab 1 (outputs_tab_page) - "Outputs":** +- `obs_text_output_check_box` - "File output (For OBS / Streaming)" +- `output_label` - "Output Folder:" +- `output_folder_text_box` (TextBox) +- `output_folder_browse_button` - "Browse" (wire to `output_folder_browse_button_Click`) +- `local_web_server_check_box` - "Enable Local Web Server" +- `port_label` - "Port:" +- `textBox1` (TextBox) - Port number input +- `chk_toast_notifications` - "Show Windows Toast Notifications" +- `chkSMTC` - "Windows System Media Controls (SMTC)" + +**Tab 2 (pianobar_override_tab_page) - "Pianobar Config & Auth":** +- `chkOverrideCredentials` - "Enable Profile Credentials..." (wire to `chkOverrideCredentials_CheckedChanged`) +- `txtConfigFilePathLabel` - "Config Save Path:" +- `txtConfigFilePath` (TextBox) +- `btnBrowseConfigFile` - "Browse" (wire to `btnBrowseConfigFile_Click`) +- `UsernameLabel` - "Username / Email:" +- `txtPianobarUser` (TextBox) +- `PasswordLabel` - "Password:" +- `txtPianobarPass` (TextBox) - Set UseSystemPasswordChar = true + +**Dialog Buttons:** +- Add OK/Cancel buttons and wire appropriately to `btnSave_Click` and `btnCancel_Click` + +### StationForm Designer Controls Required + +Open `StationForm.cs` in the Form Designer and add these controls: + +**GroupBox (station_list_group_box) - "Available Stations":** +- `search_label` - "Search:" +- `textBox1` (TextBox) - Search box (wire to `textBox1_TextChanged`) +- `lstStations` (ListBox) - Station list + +**Buttons:** +- `refresh_button` - "Refresh stations" (optional) +- `btnSelectStation` - "Select Station" (wire to `btnSelectStation_Click`) +- `btnCancelStation` - "Cancel" (wire to `btnCancelStation_Click`) + +## Quick Fix Steps + +1. **Rebuild Designer Files:** + - Open Visual Studio + - Open each form (Main.cs, Settings.cs, StationForm.cs) in the Designer + - Right-click the form surface ? "View Code" + - Close and reopen the Designer view + - This should regenerate the Designer files properly + +2. **Alternative - Manual Constructor Fix:** + If the Designer files are corrupted, you can temporarily comment out the constructors in the .cs files and let Visual Studio regenerate them. + +3. **Verify Event Wiring:** + - In the Designer, select each control + - In Properties window, click the Events icon (lightning bolt) + - Double-click the appropriate event (e.g., Click, TextChanged) + - This will wire up the event handlers automatically + +## Usage Instructions + +### First Run + +1. Launch the application +2. Go to File ? Settings +3. Create a new profile (click "+ New") +4. Configure output settings (text file, web server, etc.) +5. (Optional) Enable credential override and set Pianobar username/password +6. Click OK to save + +### Connecting to Pianobar + +1. Ensure `pianobar.exe` is in your PATH or specify full path +2. Click "Connect" button +3. Status indicator will show: + - ?? Red = Disconnected + - ?? Green = Connected (no outputs active) + - ?? Blue = Connected (outputs active) + +### Playback Control + +- Use "Play / Pause" button to toggle playback +- Use "Skip" button to skip current track +- Click "Stations" to open station picker + +### Profile Switching + +- Go to File ? Profiles +- Select a different profile (checkmark shows active) +- Reconnect to apply new profile settings + +## Technical Details + +### Status Bar Behavior + +The center status zone alternates every 10 seconds between: +- Profile name display +- Current station display + +### Output Mechanisms + +**Text File Output:** +- Writes song metadata to specified text file +- Updates on each song change +- Format: Title, Artist, Album, Album Art URL, Timestamp + +**Web Server:** +- Starts HTTP listener on specified port +- Serves JSON at `http://localhost:/` +- JSON structure: `{ title, artist, album, albumArtUrl, timestamp }` + +**Toast Notifications & SMTC:** +- Currently placeholder implementations +- Requires Windows SDK references to fully implement +- See TODO comments in OutputManager.cs + +### Pianobar Communication Protocol + +The app listens for these patterns in Pianobar output: +- Song metadata: Lines containing "Title:", "Artist:", "Album:", "coverArt:" +- Playing indicator: `|>` prefix +- Paused indicator: `||` prefix +- Station selection: `| Station "..."` + +## Troubleshooting + +### "InitializeComponent does not exist" +- The Designer files need to be regenerated +- Open each form in the Designer view +- Save the forms to regenerate Designer code + +### "Pianobar is already running" +- Close existing Pianobar instances +- The app detects running processes to avoid conflicts + +### Config file not generated +- Ensure "Override Credentials" is checked +- Verify config file path is valid +- Check write permissions to the target directory + +### Web server won't start +- Check if port is already in use +- Try a different port number +- Run as administrator if using port < 1024 + +## Future Enhancements + +1. **SMTC Integration:** + - Add reference to Windows.Media + - Implement SystemMediaTransportControls + - Handle media key events + +2. **Toast Notifications:** + - Add reference to Windows.UI.Notifications + - Implement ToastNotificationManager + - Create custom toast templates with album art + +3. **Album Art Display:** + - Download album art from URL + - Display in `album_art_picture_box` + - Cache images locally + +4. **Song Info Display:** + - Populate `info_table_Panel` with metadata + - Show song progress + - Display station info + +## File Structure + +``` +PainoBar Helper/ +??? Main.cs # Main form implementation +??? Main.Designer.cs # Main form designer (regenerate) +??? Settings.cs # Settings form implementation +??? Settings.Designer.cs # Settings designer (regenerate) +??? StationForm.cs # Station picker implementation +??? StationForm.Designer.cs # Station designer (regenerate) +??? ProfileManager.cs # Profile management backend +??? PianobarManager.cs # Pianobar process manager +??? OutputManager.cs # Output mechanisms handler +??? Program.cs # Application entry point +??? IMPLEMENTATION_GUIDE.md # This file +``` + +## Contact & Support + +For issues or questions about this implementation, refer to the inline code comments or the method documentation in each class. + +All public methods include XML documentation comments explaining their purpose and parameters. diff --git a/Main.Designer.cs b/Main.Designer.cs index c856021..d8bfb20 100644 --- a/Main.Designer.cs +++ b/Main.Designer.cs @@ -37,12 +37,16 @@ connect_button = new Button(); disconnect_button = new Button(); statusStrip1 = new StatusStrip(); + statusLabelLeft = new ToolStripStatusLabel(); + statusLabelCenter = new ToolStripStatusLabel(); + statusLabelRight = new ToolStripStatusLabel(); stations_button = new Button(); groupBox1 = new GroupBox(); btnPlayPause = new Button(); btnSkip = new Button(); menuStrip1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)album_art_picture_box).BeginInit(); + statusStrip1.SuspendLayout(); groupBox1.SuspendLayout(); SuspendLayout(); // @@ -67,6 +71,7 @@ settingsToolStripMenuItem.Name = "settingsToolStripMenuItem"; settingsToolStripMenuItem.Size = new Size(124, 22); settingsToolStripMenuItem.Text = "Settings"; + settingsToolStripMenuItem.Click += menuSettings_Click; // // profilesToolStripMenuItem // @@ -106,6 +111,7 @@ connect_button.TabIndex = 4; connect_button.Text = "Connect"; connect_button.UseVisualStyleBackColor = true; + connect_button.Click += btnConnect_Click; // // disconnect_button // @@ -115,15 +121,37 @@ disconnect_button.TabIndex = 5; disconnect_button.Text = "Disconnect"; disconnect_button.UseVisualStyleBackColor = true; + disconnect_button.Click += btnDisconnect_Click; // // statusStrip1 // + statusStrip1.Items.AddRange(new ToolStripItem[] { statusLabelLeft, statusLabelCenter, statusLabelRight }); statusStrip1.Location = new Point(0, 377); statusStrip1.Name = "statusStrip1"; statusStrip1.Size = new Size(666, 22); statusStrip1.TabIndex = 6; statusStrip1.Text = "statusStrip1"; // + // statusLabelLeft + // + statusLabelLeft.Name = "statusLabelLeft"; + statusLabelLeft.Size = new Size(100, 17); + statusLabelLeft.Text = "● Disconnected"; + statusLabelLeft.ForeColor = Color.Red; + // + // statusLabelCenter + // + statusLabelCenter.Name = "statusLabelCenter"; + statusLabelCenter.Size = new Size(451, 17); + statusLabelCenter.Spring = true; + statusLabelCenter.Text = "Profile: None"; + // + // statusLabelRight + // + statusLabelRight.Name = "statusLabelRight"; + statusLabelRight.Size = new Size(100, 17); + statusLabelRight.Text = "Ready"; + // // stations_button // stations_button.Location = new Point(385, 253); @@ -132,6 +160,7 @@ stations_button.TabIndex = 7; stations_button.Text = "Stations"; stations_button.UseVisualStyleBackColor = true; + stations_button.Click += btnStations_Click; // // groupBox1 // @@ -152,6 +181,7 @@ btnPlayPause.TabIndex = 0; btnPlayPause.Text = "Play / Pause"; btnPlayPause.UseVisualStyleBackColor = true; + btnPlayPause.Click += btnPlayPause_Click; // // btnSkip // @@ -161,6 +191,7 @@ btnSkip.TabIndex = 1; btnSkip.Text = "Skip"; btnSkip.UseVisualStyleBackColor = true; + btnSkip.Click += btnSkip_Click; // // Main // @@ -181,6 +212,8 @@ menuStrip1.ResumeLayout(false); menuStrip1.PerformLayout(); ((System.ComponentModel.ISupportInitialize)album_art_picture_box).EndInit(); + statusStrip1.ResumeLayout(false); + statusStrip1.PerformLayout(); groupBox1.ResumeLayout(false); ResumeLayout(false); PerformLayout(); @@ -201,5 +234,8 @@ private GroupBox groupBox1; private Button btnPlayPause; private Button btnSkip; + private ToolStripStatusLabel statusLabelLeft; + private ToolStripStatusLabel statusLabelCenter; + private ToolStripStatusLabel statusLabelRight; } } diff --git a/Main.cs b/Main.cs index 20952ac..06e760c 100644 --- a/Main.cs +++ b/Main.cs @@ -2,9 +2,404 @@ namespace PainoBar_Helper { public partial class Main : Form { + private readonly ProfileManager _profileManager; + private readonly PianobarManager _pianobarManager; + private OutputManager? _outputManager; + private System.Windows.Forms.Timer? _statusTimer; + private bool _statusToggle = false; + + // Connection status enum + private enum ConnectionStatus + { + Disconnected, // Red + ConnectedIdle, // Green + ConnectedActive // Blue + } + + private ConnectionStatus _currentStatus = ConnectionStatus.Disconnected; + public Main() { InitializeComponent(); + + _profileManager = new ProfileManager(); + _pianobarManager = new PianobarManager(); + + InitializeMenus(); + InitializeStatusBar(); + InitializeEvents(); + UpdateProfilesMenu(); + UpdateConnectionStatus(ConnectionStatus.Disconnected); } + + #region Initialization + + private void InitializeMenus() + { + // File menu is assumed to be created in the Designer + // This method sets up dynamic profile menu items + } + + private void InitializeStatusBar() + { + // Status bar zones are assumed to be created in the Designer: + // - statusLabelLeft: Connection indicator + // - statusLabelCenter: Alternating status (Profile/Station) + // - statusLabelRight: Event feed + + _statusTimer = new System.Windows.Forms.Timer(); + _statusTimer.Interval = 10000; // 10 seconds + _statusTimer.Tick += StatusTimer_Tick; + _statusTimer.Start(); + + UpdateCenterStatus(); + } + + private void InitializeEvents() + { + // Pianobar events + _pianobarManager.Connected += PianobarManager_Connected; + _pianobarManager.Disconnected += PianobarManager_Disconnected; + _pianobarManager.SongChanged += PianobarManager_SongChanged; + _pianobarManager.StationChanged += PianobarManager_StationChanged; + _pianobarManager.EventOccurred += PianobarManager_EventOccurred; + + // Profile events + _profileManager.ProfilesChanged += ProfileManager_ProfilesChanged; + _profileManager.ActiveProfileChanged += ProfileManager_ActiveProfileChanged; + } + + #endregion + + #region Button Event Handlers + + // This method should be wired to btnConnect.Click in the Designer + public void btnConnect_Click(object? sender, EventArgs e) + { + if (_pianobarManager.IsConnected) + { + MessageBox.Show("Already connected to Pianobar.", "Already Connected", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + try + { + // Get pianobar.exe path (you may want to add a setting for this) + string pianobarPath = "pianobar.exe"; // Default, assumes it's in PATH + string? configPath = _profileManager.GetActiveConfigPath(); + + _pianobarManager.Connect(pianobarPath, configPath); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to connect to Pianobar:\n{ex.Message}", "Connection Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + // This method should be wired to btnDisconnect.Click in the Designer + public void btnDisconnect_Click(object? sender, EventArgs e) + { + if (!_pianobarManager.IsConnected) + { + MessageBox.Show("Not connected to Pianobar.", "Not Connected", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + _pianobarManager.Disconnect(); + _outputManager?.Stop(); + } + + // This method should be wired to btnPlayPause.Click in the Designer + public void btnPlayPause_Click(object? sender, EventArgs e) + { + if (!_pianobarManager.IsConnected) + { + MessageBox.Show("Not connected to Pianobar.", "Not Connected", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + try + { + _pianobarManager.PlayPause(); + UpdatePlayPauseButton(); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to toggle play/pause:\n{ex.Message}", "Command Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + // This method should be wired to btnSkip.Click in the Designer + public void btnSkip_Click(object? sender, EventArgs e) + { + if (!_pianobarManager.IsConnected) + { + MessageBox.Show("Not connected to Pianobar.", "Not Connected", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + try + { + _pianobarManager.Skip(); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to skip track:\n{ex.Message}", "Command Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + // This method should be wired to btnStations.Click in the Designer (or menu item) + public void btnStations_Click(object? sender, EventArgs e) + { + if (!_pianobarManager.IsConnected) + { + MessageBox.Show("Not connected to Pianobar.", "Not Connected", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + var stationForm = new StationForm(_pianobarManager); + stationForm.ShowDialog(this); + } + + #endregion + + #region Menu Event Handlers + + // This method should be wired to the Settings menu item click + public void menuSettings_Click(object? sender, EventArgs e) + { + var activeProfile = _profileManager.GetActiveProfile(); + var settingsForm = new SettingsForm(_profileManager, activeProfile?.Settings ?? new ProfileSettings()); + if (settingsForm.ShowDialog(this) == DialogResult.OK) + { + // Settings were saved, restart output manager if connected + if (_pianobarManager.IsConnected) + { + RestartOutputManager(); + } + } + } + + private void ProfileManager_ProfilesChanged(object? sender, EventArgs e) + { + UpdateProfilesMenu(); + } + + private void ProfileManager_ActiveProfileChanged(object? sender, EventArgs e) + { + UpdateProfilesMenu(); + UpdateCenterStatus(); + } + + // Call this method to build the Profiles submenu dynamically + private void UpdateProfilesMenu() + { + // Assumes menuProfiles (profilesToolStripMenuItem) is a ToolStripMenuItem in your File menu + // Created in the Designer + if (profilesToolStripMenuItem == null) + return; + + profilesToolStripMenuItem.DropDownItems.Clear(); + + foreach (var profile in _profileManager.Profiles) + { + var menuItem = new ToolStripMenuItem(profile.Name); + menuItem.Tag = profile.Name; + menuItem.Click += ProfileMenuItem_Click; + + if (profile.Name == _profileManager.ActiveProfileName) + menuItem.Checked = true; + + profilesToolStripMenuItem.DropDownItems.Add(menuItem); + } + } + + private void ProfileMenuItem_Click(object? sender, EventArgs e) + { + if (sender is ToolStripMenuItem menuItem && menuItem.Tag is string profileName) + { + _profileManager.SetActiveProfile(profileName); + } + } + + #endregion + + #region Pianobar Event Handlers + + private void PianobarManager_Connected(object? sender, EventArgs e) + { + Invoke(() => + { + var activeProfile = _profileManager.GetActiveProfile(); + if (activeProfile != null) + { + _outputManager = new OutputManager(activeProfile.Settings); + _outputManager.ActiveStateChanged += OutputManager_ActiveStateChanged; + _outputManager.Start(); + } + + UpdateConnectionStatus( + _outputManager?.IsActive == true ? ConnectionStatus.ConnectedActive : ConnectionStatus.ConnectedIdle + ); + UpdatePlayPauseButton(); + }); + } + + private void PianobarManager_Disconnected(object? sender, EventArgs e) + { + Invoke(() => + { + _outputManager?.Stop(); + _outputManager = null; + UpdateConnectionStatus(ConnectionStatus.Disconnected); + UpdatePlayPauseButton(); + }); + } + + private void PianobarManager_SongChanged(object? sender, SongChangedEventArgs e) + { + Invoke(() => + { + _outputManager?.UpdateSongMetadata(e); + }); + } + + private void PianobarManager_StationChanged(object? sender, string e) + { + // Station change is handled via event feed + UpdateCenterStatus(); + } + + private void PianobarManager_EventOccurred(object? sender, PianobarEventArgs e) + { + Invoke(() => + { + // Update right status zone with event text + if (statusLabelRight != null) + { + statusLabelRight.Text = e.EventText; + } + }); + } + + private void OutputManager_ActiveStateChanged(object? sender, EventArgs e) + { + Invoke(() => + { + if (_pianobarManager.IsConnected) + { + UpdateConnectionStatus( + _outputManager?.IsActive == true ? ConnectionStatus.ConnectedActive : ConnectionStatus.ConnectedIdle + ); + } + }); + } + + #endregion + + #region UI Update Methods + + private void UpdateConnectionStatus(ConnectionStatus status) + { + _currentStatus = status; + + if (statusLabelLeft == null) + return; + + switch (status) + { + case ConnectionStatus.Disconnected: + statusLabelLeft.Text = "? Disconnected"; + statusLabelLeft.ForeColor = Color.Red; + break; + case ConnectionStatus.ConnectedIdle: + statusLabelLeft.Text = "? Connected (Idle)"; + statusLabelLeft.ForeColor = Color.Green; + break; + case ConnectionStatus.ConnectedActive: + statusLabelLeft.Text = "? Connected (Active)"; + statusLabelLeft.ForeColor = Color.Blue; + break; + } + } + + private void UpdatePlayPauseButton() + { + // Uses btnPlayPause from the Designer + if (btnPlayPause == null) + return; + + if (!_pianobarManager.IsConnected) + { + btnPlayPause.Text = "Play"; + return; + } + + btnPlayPause.Text = _pianobarManager.IsPlaying ? "Pause" : "Play"; + } + + private void StatusTimer_Tick(object? sender, EventArgs e) + { + _statusToggle = !_statusToggle; + UpdateCenterStatus(); + } + + private void UpdateCenterStatus() + { + if (statusLabelCenter == null) + return; + + if (_statusToggle) + { + // Show profile + string profileName = _profileManager.ActiveProfileName ?? "None"; + statusLabelCenter.Text = $"Profile: {profileName}"; + } + else + { + // Show station + string station = _pianobarManager.CurrentStation ?? "None"; + statusLabelCenter.Text = $"Station: {station}"; + } + } + + private void RestartOutputManager() + { + _outputManager?.Stop(); + + var activeProfile = _profileManager.GetActiveProfile(); + if (activeProfile != null && _pianobarManager.IsConnected) + { + _outputManager = new OutputManager(activeProfile.Settings); + _outputManager.ActiveStateChanged += OutputManager_ActiveStateChanged; + _outputManager.Start(); + + UpdateConnectionStatus( + _outputManager.IsActive ? ConnectionStatus.ConnectedActive : ConnectionStatus.ConnectedIdle + ); + } + } + + #endregion + + #region Cleanup + + protected override void OnFormClosing(FormClosingEventArgs e) + { + _statusTimer?.Stop(); + _pianobarManager?.Disconnect(); + _outputManager?.Dispose(); + base.OnFormClosing(e); + } + + #endregion } } diff --git a/OutputManager.cs b/OutputManager.cs new file mode 100644 index 0000000..4404d4a --- /dev/null +++ b/OutputManager.cs @@ -0,0 +1,216 @@ +using System.Net; +using System.Text; +using System.Text.Json; + +namespace PainoBar_Helper +{ + /// + /// Manages various output mechanisms for song metadata + /// + public class OutputManager : IDisposable + { + private readonly ProfileSettings _settings; + private HttpListener? _webServer; + private string? _currentMetadataJson; + private bool _isActive; + + public OutputManager(ProfileSettings settings) + { + _settings = settings; + } + + public event EventHandler? ActiveStateChanged; + + public bool IsActive + { + get => _isActive; + private set + { + if (_isActive != value) + { + _isActive = value; + ActiveStateChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + public void Start() + { + Stop(); + + if (_settings.EnableWebServer) + StartWebServer(); + + UpdateActiveState(); + } + + public void Stop() + { + StopWebServer(); + UpdateActiveState(); + } + + public void UpdateSongMetadata(SongChangedEventArgs metadata) + { + var metadataObj = new + { + title = metadata.Title, + artist = metadata.Artist, + album = metadata.Album, + albumArtUrl = metadata.AlbumArtUrl, + timestamp = DateTime.Now + }; + + _currentMetadataJson = JsonSerializer.Serialize(metadataObj, new JsonSerializerOptions { WriteIndented = true }); + + if (_settings.EnableTextFile && !string.IsNullOrEmpty(_settings.TextFilePath)) + WriteTextFile(metadata); + + if (_settings.EnableToastNotifications) + ShowToastNotification(metadata); + + if (_settings.EnableSMTC) + UpdateSMTC(metadata); + } + + private void WriteTextFile(SongChangedEventArgs metadata) + { + try + { + var dir = Path.GetDirectoryName(_settings.TextFilePath); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + + var sb = new StringBuilder(); + sb.AppendLine($"Title: {metadata.Title}"); + sb.AppendLine($"Artist: {metadata.Artist}"); + sb.AppendLine($"Album: {metadata.Album}"); + sb.AppendLine($"Album Art URL: {metadata.AlbumArtUrl}"); + sb.AppendLine($"Updated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + + File.WriteAllText(_settings.TextFilePath, sb.ToString()); + } + catch + { + // Silently fail + } + } + + private void StartWebServer() + { + try + { + _webServer = new HttpListener(); + _webServer.Prefixes.Add($"http://localhost:{_settings.WebServerPort}/"); + _webServer.Start(); + + Task.Run(() => WebServerLoop()); + } + catch + { + _webServer = null; + } + } + + private void StopWebServer() + { + if (_webServer != null) + { + try + { + _webServer.Stop(); + _webServer.Close(); + } + catch { } + finally + { + _webServer = null; + } + } + } + + private async void WebServerLoop() + { + while (_webServer != null && _webServer.IsListening) + { + try + { + var context = await _webServer.GetContextAsync(); + HandleWebRequest(context); + } + catch + { + break; + } + } + } + + private void HandleWebRequest(HttpListenerContext context) + { + try + { + var response = context.Response; + response.ContentType = "application/json"; + response.ContentEncoding = Encoding.UTF8; + + string responseData = _currentMetadataJson ?? "{}"; + byte[] buffer = Encoding.UTF8.GetBytes(responseData); + + response.ContentLength64 = buffer.Length; + response.OutputStream.Write(buffer, 0, buffer.Length); + response.OutputStream.Close(); + } + catch { } + } + + private void ShowToastNotification(SongChangedEventArgs metadata) + { + // Windows Toast Notification implementation placeholder + // In a full implementation, you would use Windows.UI.Notifications + // For now, this is a placeholder that can be extended + try + { + // TODO: Implement using Windows.UI.Notifications ToastNotificationManager + // This requires adding Windows SDK references + } + catch { } + } + + private void UpdateSMTC(SongChangedEventArgs metadata) + { + // Windows System Media Transport Controls implementation placeholder + // In a full implementation, you would use Windows.Media.SystemMediaTransportControls + // For now, this is a placeholder that can be extended + try + { + // TODO: Implement using Windows.Media.SystemMediaTransportControls + // This requires adding Windows SDK references + } + catch { } + } + + private void UpdateActiveState() + { + bool anyActive = false; + + if (_settings.EnableTextFile && !string.IsNullOrEmpty(_settings.TextFilePath)) + anyActive = true; + + if (_settings.EnableWebServer && _webServer != null && _webServer.IsListening) + anyActive = true; + + if (_settings.EnableToastNotifications) + anyActive = true; + + if (_settings.EnableSMTC) + anyActive = true; + + IsActive = anyActive; + } + + public void Dispose() + { + Stop(); + } + } +} diff --git a/PianobarManager.cs b/PianobarManager.cs new file mode 100644 index 0000000..08894fd --- /dev/null +++ b/PianobarManager.cs @@ -0,0 +1,328 @@ +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; + +namespace PainoBar_Helper +{ + /// + /// Manages the Pianobar process lifecycle and communication + /// + public class PianobarManager : IDisposable + { + private Process? _process; + private StreamWriter? _inputWriter; + private bool _isConnected; + private readonly object _lockObject = new object(); + + public event EventHandler? SongChanged; + public event EventHandler? StationChanged; + public event EventHandler? EventOccurred; + public event EventHandler? Connected; + public event EventHandler? Disconnected; + public event EventHandler? OutputReceived; + + public bool IsConnected + { + get { lock (_lockObject) return _isConnected; } + private set + { + bool changed = false; + lock (_lockObject) + { + if (_isConnected != value) + { + _isConnected = value; + changed = true; + } + } + if (changed) + { + if (value) + Connected?.Invoke(this, EventArgs.Empty); + else + Disconnected?.Invoke(this, EventArgs.Empty); + } + } + } + + public string? CurrentSong { get; private set; } + public string? CurrentArtist { get; private set; } + public string? CurrentAlbum { get; private set; } + public string? CurrentAlbumArtUrl { get; private set; } + public string? CurrentStation { get; private set; } + public bool IsPlaying { get; private set; } = true; + + public void Connect(string pianobarPath, string? configPath = null) + { + if (IsConnected) + throw new InvalidOperationException("Already connected to Pianobar."); + + // Check if pianobar is already running + var existingProcesses = Process.GetProcessesByName("pianobar"); + if (existingProcesses.Length > 0) + { + foreach (var proc in existingProcesses) + proc.Dispose(); + throw new InvalidOperationException("Pianobar is already running. Please close existing instances."); + } + + try + { + var startInfo = new ProcessStartInfo + { + FileName = pianobarPath, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + + if (!string.IsNullOrEmpty(configPath)) + { + startInfo.Arguments = $"-c \"{configPath}\""; + } + + _process = new Process { StartInfo = startInfo }; + _process.OutputDataReceived += Process_OutputDataReceived; + _process.ErrorDataReceived += Process_ErrorDataReceived; + _process.Exited += Process_Exited; + _process.EnableRaisingEvents = true; + + _process.Start(); + _inputWriter = _process.StandardInput; + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + + IsConnected = true; + RaiseEvent("Connected to Pianobar"); + } + catch (Exception ex) + { + Cleanup(); + throw new InvalidOperationException($"Failed to start Pianobar: {ex.Message}", ex); + } + } + + public void Disconnect() + { + if (!IsConnected) + return; + + Cleanup(); + IsConnected = false; + RaiseEvent("Disconnected from Pianobar"); + } + + public void SendCommand(char command) + { + if (!IsConnected || _inputWriter == null) + throw new InvalidOperationException("Not connected to Pianobar."); + + try + { + _inputWriter.WriteLine(command); + _inputWriter.Flush(); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to send command: {ex.Message}", ex); + } + } + + public void SendCommand(string command) + { + if (!IsConnected || _inputWriter == null) + throw new InvalidOperationException("Not connected to Pianobar."); + + try + { + _inputWriter.WriteLine(command); + _inputWriter.Flush(); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to send command: {ex.Message}", ex); + } + } + + public void PlayPause() + { + SendCommand('p'); + IsPlaying = !IsPlaying; + RaiseEvent(IsPlaying ? "Resumed" : "Paused"); + } + + public void Skip() + { + SendCommand('n'); + RaiseEvent("Skipped Track"); + } + + public void ShowStations() + { + SendCommand('s'); + } + + private void Process_OutputDataReceived(object? sender, DataReceivedEventArgs e) + { + if (string.IsNullOrEmpty(e.Data)) + return; + + string line = e.Data; + OutputReceived?.Invoke(this, line); + + ParseOutput(line); + } + + private void Process_ErrorDataReceived(object? sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrEmpty(e.Data)) + OutputReceived?.Invoke(this, e.Data); + } + + private void Process_Exited(object? sender, EventArgs e) + { + IsConnected = false; + RaiseEvent("Pianobar process exited"); + } + + private void ParseOutput(string line) + { + try + { + // Parse song information + if (line.Contains("|>")) + { + // Song is playing + IsPlaying = true; + ParseSongLine(line); + } + else if (line.Contains("||")) + { + // Song is paused + IsPlaying = false; + } + + // Parse station selection + var stationMatch = Regex.Match(line, @"\|\s*Station\s+""(.+?)"""); + if (stationMatch.Success) + { + CurrentStation = stationMatch.Groups[1].Value; + StationChanged?.Invoke(this, CurrentStation); + RaiseEvent("Station Selected"); + } + + // Parse song metadata + if (line.Contains("Title:")) + { + var match = Regex.Match(line, @"Title:\s*(.+)"); + if (match.Success) + CurrentSong = match.Groups[1].Value.Trim(); + } + else if (line.Contains("Artist:")) + { + var match = Regex.Match(line, @"Artist:\s*(.+)"); + if (match.Success) + CurrentArtist = match.Groups[1].Value.Trim(); + } + else if (line.Contains("Album:")) + { + var match = Regex.Match(line, @"Album:\s*(.+)"); + if (match.Success) + CurrentAlbum = match.Groups[1].Value.Trim(); + } + else if (line.Contains("coverArt")) + { + var match = Regex.Match(line, @"coverArt:\s*(.+)"); + if (match.Success) + { + CurrentAlbumArtUrl = match.Groups[1].Value.Trim(); + TriggerSongChanged(); + } + } + } + catch + { + // Ignore parsing errors + } + } + + private void ParseSongLine(string line) + { + // Parse format: |> "Song Title" by "Artist" on "Album" + var match = Regex.Match(line, @"\|>\s+""(.+?)""\s+by\s+""(.+?)""\s+on\s+""(.+?)"""); + if (match.Success) + { + CurrentSong = match.Groups[1].Value; + CurrentArtist = match.Groups[2].Value; + CurrentAlbum = match.Groups[3].Value; + TriggerSongChanged(); + } + } + + private void TriggerSongChanged() + { + if (!string.IsNullOrEmpty(CurrentSong) && !string.IsNullOrEmpty(CurrentArtist)) + { + SongChanged?.Invoke(this, new SongChangedEventArgs + { + Title = CurrentSong, + Artist = CurrentArtist, + Album = CurrentAlbum, + AlbumArtUrl = CurrentAlbumArtUrl + }); + RaiseEvent("Song Changed"); + } + } + + private void RaiseEvent(string eventText) + { + EventOccurred?.Invoke(this, new PianobarEventArgs { EventText = eventText }); + } + + private void Cleanup() + { + try + { + if (_process != null) + { + _process.OutputDataReceived -= Process_OutputDataReceived; + _process.ErrorDataReceived -= Process_ErrorDataReceived; + _process.Exited -= Process_Exited; + + _inputWriter?.Close(); + _inputWriter = null; + + // Don't kill the process, just detach + _process.Close(); + _process = null; + } + } + catch + { + // Ignore cleanup errors + } + } + + public void Dispose() + { + Disconnect(); + } + } + + public class SongChangedEventArgs : EventArgs + { + public string? Title { get; set; } + public string? Artist { get; set; } + public string? Album { get; set; } + public string? AlbumArtUrl { get; set; } + } + + public class PianobarEventArgs : EventArgs + { + public string EventText { get; set; } = string.Empty; + } +} diff --git a/ProfileManager.cs b/ProfileManager.cs new file mode 100644 index 0000000..8676628 --- /dev/null +++ b/ProfileManager.cs @@ -0,0 +1,219 @@ +using System.Text; +using System.Text.Json; + +namespace PainoBar_Helper +{ + /// + /// Manages Pianobar profiles including creation, deletion, and configuration file generation + /// + public class ProfileManager + { + private const string ProfilesFileName = "profiles.json"; + private readonly string _profilesFilePath; + private List _profiles; + private string? _activeProfileName; + + public ProfileManager() + { + string appDataPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "PianobarHelper" + ); + Directory.CreateDirectory(appDataPath); + _profilesFilePath = Path.Combine(appDataPath, ProfilesFileName); + _profiles = new List(); + LoadProfiles(); + } + + public event EventHandler? ProfilesChanged; + public event EventHandler? ActiveProfileChanged; + + public IReadOnlyList Profiles => _profiles.AsReadOnly(); + public string? ActiveProfileName => _activeProfileName; + + public Profile? GetActiveProfile() + { + if (string.IsNullOrEmpty(_activeProfileName)) + return null; + return _profiles.FirstOrDefault(p => p.Name == _activeProfileName); + } + + public void SetActiveProfile(string profileName) + { + var profile = _profiles.FirstOrDefault(p => p.Name == profileName); + if (profile == null) + throw new ArgumentException($"Profile '{profileName}' does not exist."); + + _activeProfileName = profileName; + SaveProfiles(); + ActiveProfileChanged?.Invoke(this, EventArgs.Empty); + } + + public void CreateProfile(string name, ProfileSettings settings) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Profile name cannot be empty."); + + if (_profiles.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + throw new InvalidOperationException($"Profile '{name}' already exists."); + + var profile = new Profile + { + Name = name, + Settings = settings + }; + + _profiles.Add(profile); + + if (_profiles.Count == 1) + _activeProfileName = name; + + SaveProfiles(); + ProfilesChanged?.Invoke(this, EventArgs.Empty); + + if (settings.OverrideCredentials && !string.IsNullOrEmpty(settings.ConfigFilePath)) + GenerateConfigFile(profile); + } + + public void UpdateProfile(string name, ProfileSettings settings) + { + var profile = _profiles.FirstOrDefault(p => p.Name == name); + if (profile == null) + throw new ArgumentException($"Profile '{name}' does not exist."); + + profile.Settings = settings; + SaveProfiles(); + ProfilesChanged?.Invoke(this, EventArgs.Empty); + + if (settings.OverrideCredentials && !string.IsNullOrEmpty(settings.ConfigFilePath)) + GenerateConfigFile(profile); + } + + public void DeleteProfile(string name) + { + var profile = _profiles.FirstOrDefault(p => p.Name == name); + if (profile == null) + return; + + _profiles.Remove(profile); + + if (_activeProfileName == name) + { + _activeProfileName = _profiles.FirstOrDefault()?.Name; + ActiveProfileChanged?.Invoke(this, EventArgs.Empty); + } + + SaveProfiles(); + ProfilesChanged?.Invoke(this, EventArgs.Empty); + } + + private void LoadProfiles() + { + if (!File.Exists(_profilesFilePath)) + { + _profiles = new List(); + return; + } + + try + { + string json = File.ReadAllText(_profilesFilePath); + var data = JsonSerializer.Deserialize(json); + if (data != null) + { + _profiles = data.Profiles ?? new List(); + _activeProfileName = data.ActiveProfile; + } + } + catch + { + _profiles = new List(); + } + } + + private void SaveProfiles() + { + var data = new ProfileData + { + Profiles = _profiles, + ActiveProfile = _activeProfileName + }; + + string json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(_profilesFilePath, json); + } + + private void GenerateConfigFile(Profile profile) + { + var settings = profile.Settings; + if (string.IsNullOrEmpty(settings.ConfigFilePath)) + return; + + try + { + var configDir = Path.GetDirectoryName(settings.ConfigFilePath); + if (!string.IsNullOrEmpty(configDir)) + Directory.CreateDirectory(configDir); + + var sb = new StringBuilder(); + sb.AppendLine("# Pianobar configuration file"); + sb.AppendLine($"# Generated by Pianobar Helper for profile: {profile.Name}"); + sb.AppendLine(); + + if (settings.OverrideCredentials) + { + if (!string.IsNullOrEmpty(settings.PianobarUser)) + sb.AppendLine($"user = {settings.PianobarUser}"); + if (!string.IsNullOrEmpty(settings.PianobarPassword)) + sb.AppendLine($"password = {settings.PianobarPassword}"); + sb.AppendLine(); + } + + File.WriteAllText(settings.ConfigFilePath, sb.ToString()); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to generate config file: {ex.Message}", ex); + } + } + + public string? GetActiveConfigPath() + { + var activeProfile = GetActiveProfile(); + if (activeProfile?.Settings.OverrideCredentials == true) + return activeProfile.Settings.ConfigFilePath; + return null; + } + } + + public class Profile + { + public string Name { get; set; } = string.Empty; + public ProfileSettings Settings { get; set; } = new ProfileSettings(); + } + + public class ProfileSettings + { + // Output Settings + public bool EnableTextFile { get; set; } + public string TextFilePath { get; set; } = string.Empty; + + public bool EnableWebServer { get; set; } + public int WebServerPort { get; set; } = 8080; + + public bool EnableToastNotifications { get; set; } + public bool EnableSMTC { get; set; } + + // Pianobar Config & Auth + public bool OverrideCredentials { get; set; } + public string ConfigFilePath { get; set; } = string.Empty; + public string PianobarUser { get; set; } = string.Empty; + public string PianobarPassword { get; set; } = string.Empty; + } + + internal class ProfileData + { + public List? Profiles { get; set; } + public string? ActiveProfile { get; set; } + } +} diff --git a/Settings.Designer.cs b/Settings.Designer.cs index 2f701cd..514cc8a 100644 --- a/Settings.Designer.cs +++ b/Settings.Designer.cs @@ -1,6 +1,6 @@ namespace PainoBar_Helper { - partial class settings_form + partial class SettingsForm { /// /// Required designer variable. @@ -95,6 +95,7 @@ profiles_combo_box_selector.Name = "profiles_combo_box_selector"; profiles_combo_box_selector.Size = new Size(199, 25); profiles_combo_box_selector.TabIndex = 1; + profiles_combo_box_selector.SelectedIndexChanged += profiles_combo_box_selector_SelectedIndexChanged; // // new_profile_button // @@ -104,6 +105,7 @@ new_profile_button.TabIndex = 2; new_profile_button.Text = "+ New"; new_profile_button.UseVisualStyleBackColor = true; + new_profile_button.Click += new_profile_button_Click; // // delete_profile_button // @@ -113,6 +115,7 @@ delete_profile_button.TabIndex = 3; delete_profile_button.Text = "Delete"; delete_profile_button.UseVisualStyleBackColor = true; + delete_profile_button.Click += delete_profile_button_Click; // // profile_name_label // @@ -251,6 +254,7 @@ output_folder_browse_button.TabIndex = 3; output_folder_browse_button.Text = "Browse"; output_folder_browse_button.UseVisualStyleBackColor = true; + output_folder_browse_button.Click += output_folder_browse_button_Click; // // local_web_server_check_box // @@ -307,6 +311,7 @@ chkOverrideCredentials.TabIndex = 0; chkOverrideCredentials.Text = "Enable Profile Credentials and Custom Config Generation"; chkOverrideCredentials.UseVisualStyleBackColor = true; + chkOverrideCredentials.CheckedChanged += chkOverrideCredentials_CheckedChanged; // // txtConfigFilePathLabel // @@ -332,6 +337,7 @@ btnBrowseConfigFile.TabIndex = 3; btnBrowseConfigFile.Text = "Browse"; btnBrowseConfigFile.UseVisualStyleBackColor = true; + btnBrowseConfigFile.Click += btnBrowseConfigFile_Click; // // UsernameLabel // @@ -366,14 +372,14 @@ txtPianobarPass.TabIndex = 7; txtPianobarPass.UseSystemPasswordChar = true; // - // settings_form + // SettingsForm // AutoScaleDimensions = new SizeF(7F, 17F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(454, 586); Controls.Add(profile_configuration_group_box); Controls.Add(profiles_group_box); - Name = "settings_form"; + Name = "SettingsForm"; Text = "Settings"; profiles_group_box.ResumeLayout(false); profiles_group_box.PerformLayout(); diff --git a/Settings.cs b/Settings.cs index 5b4a762..af6968a 100644 --- a/Settings.cs +++ b/Settings.cs @@ -1,20 +1,352 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - namespace PainoBar_Helper { - public partial class settings_form : Form + public partial class SettingsForm : Form { - public settings_form() + private readonly ProfileManager _profileManager; + private ProfileSettings _currentSettings; + private string? _currentProfileName; + + public SettingsForm(ProfileManager profileManager, ProfileSettings currentSettings) { InitializeComponent(); + + _profileManager = profileManager; + _currentSettings = new ProfileSettings + { + EnableTextFile = currentSettings.EnableTextFile, + TextFilePath = currentSettings.TextFilePath, + EnableWebServer = currentSettings.EnableWebServer, + WebServerPort = currentSettings.WebServerPort, + EnableToastNotifications = currentSettings.EnableToastNotifications, + EnableSMTC = currentSettings.EnableSMTC, + OverrideCredentials = currentSettings.OverrideCredentials, + ConfigFilePath = currentSettings.ConfigFilePath, + PianobarUser = currentSettings.PianobarUser, + PianobarPassword = currentSettings.PianobarPassword + }; + + _currentProfileName = profileManager.ActiveProfileName; + InitializeControls(); } + + #region Initialization + + private void InitializeControls() + { + LoadProfileList(); + LoadSettingsToControls(); + } + + private void LoadProfileList() + { + // Uses profiles_combo_box_selector from the Designer + if (profiles_combo_box_selector == null) + return; + + profiles_combo_box_selector.Items.Clear(); + foreach (var profile in _profileManager.Profiles) + { + profiles_combo_box_selector.Items.Add(profile.Name); + } + + if (!string.IsNullOrEmpty(_currentProfileName)) + profiles_combo_box_selector.SelectedItem = _currentProfileName; + } + + private void LoadSettingsToControls() + { + // Output Settings (Tab 1) + if (obs_text_output_check_box != null) + obs_text_output_check_box.Checked = _currentSettings.EnableTextFile; + + if (output_folder_text_box != null) + output_folder_text_box.Text = _currentSettings.TextFilePath; + + if (local_web_server_check_box != null) + local_web_server_check_box.Checked = _currentSettings.EnableWebServer; + + if (textBox1 != null) + textBox1.Text = _currentSettings.WebServerPort.ToString(); + + if (chk_toast_notifications != null) + chk_toast_notifications.Checked = _currentSettings.EnableToastNotifications; + + if (chkSMTC != null) + chkSMTC.Checked = _currentSettings.EnableSMTC; + + // Pianobar Config (Tab 2) + if (chkOverrideCredentials != null) + { + chkOverrideCredentials.Checked = _currentSettings.OverrideCredentials; + ToggleCredentialFields(chkOverrideCredentials.Checked); + } + + if (txtConfigFilePath != null) + txtConfigFilePath.Text = _currentSettings.ConfigFilePath; + + if (txtPianobarUser != null) + txtPianobarUser.Text = _currentSettings.PianobarUser; + + if (txtPianobarPass != null) + txtPianobarPass.Text = _currentSettings.PianobarPassword; + } + + private void SaveSettingsFromControls() + { + // Output Settings + if (obs_text_output_check_box != null) + _currentSettings.EnableTextFile = obs_text_output_check_box.Checked; + + if (output_folder_text_box != null) + _currentSettings.TextFilePath = output_folder_text_box.Text; + + if (local_web_server_check_box != null) + _currentSettings.EnableWebServer = local_web_server_check_box.Checked; + + if (textBox1 != null && int.TryParse(textBox1.Text, out int port)) + _currentSettings.WebServerPort = port; + + if (chk_toast_notifications != null) + _currentSettings.EnableToastNotifications = chk_toast_notifications.Checked; + + if (chkSMTC != null) + _currentSettings.EnableSMTC = chkSMTC.Checked; + + // Pianobar Config + if (chkOverrideCredentials != null) + _currentSettings.OverrideCredentials = chkOverrideCredentials.Checked; + + if (txtConfigFilePath != null) + _currentSettings.ConfigFilePath = txtConfigFilePath.Text; + + if (txtPianobarUser != null) + _currentSettings.PianobarUser = txtPianobarUser.Text; + + if (txtPianobarPass != null) + _currentSettings.PianobarPassword = txtPianobarPass.Text; + } + + #endregion + + #region Profile Management + + // Wire this to profiles_combo_box_selector.SelectedIndexChanged in the Designer + public void profiles_combo_box_selector_SelectedIndexChanged(object? sender, EventArgs e) + { + if (sender is not ComboBox cmb || cmb.SelectedItem is not string profileName) + return; + + var profile = _profileManager.Profiles.FirstOrDefault(p => p.Name == profileName); + if (profile != null) + { + _currentProfileName = profileName; + _currentSettings = new ProfileSettings + { + EnableTextFile = profile.Settings.EnableTextFile, + TextFilePath = profile.Settings.TextFilePath, + EnableWebServer = profile.Settings.EnableWebServer, + WebServerPort = profile.Settings.WebServerPort, + EnableToastNotifications = profile.Settings.EnableToastNotifications, + EnableSMTC = profile.Settings.EnableSMTC, + OverrideCredentials = profile.Settings.OverrideCredentials, + ConfigFilePath = profile.Settings.ConfigFilePath, + PianobarUser = profile.Settings.PianobarUser, + PianobarPassword = profile.Settings.PianobarPassword + }; + LoadSettingsToControls(); + } + } + + // Wire this to new_profile_button.Click in the Designer + public void new_profile_button_Click(object? sender, EventArgs e) + { + string? profileName = PromptForProfileName("New Profile", "Enter profile name:"); + if (string.IsNullOrWhiteSpace(profileName)) + return; + + try + { + var newSettings = new ProfileSettings(); + _profileManager.CreateProfile(profileName, newSettings); + + LoadProfileList(); + if (profiles_combo_box_selector != null) + profiles_combo_box_selector.SelectedItem = profileName; + + MessageBox.Show($"Profile '{profileName}' created successfully.", "Profile Created", + MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to create profile:\n{ex.Message}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + // Wire this to delete_profile_button.Click in the Designer + public void delete_profile_button_Click(object? sender, EventArgs e) + { + if (string.IsNullOrEmpty(_currentProfileName)) + { + MessageBox.Show("No profile selected.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + var result = MessageBox.Show($"Are you sure you want to delete profile '{_currentProfileName}'?", + "Confirm Deletion", MessageBoxButtons.YesNo, MessageBoxIcon.Question); + + if (result == DialogResult.Yes) + { + try + { + _profileManager.DeleteProfile(_currentProfileName); + _currentProfileName = _profileManager.ActiveProfileName; + LoadProfileList(); + + var activeProfile = _profileManager.GetActiveProfile(); + if (activeProfile != null) + { + _currentSettings = activeProfile.Settings; + LoadSettingsToControls(); + } + + MessageBox.Show("Profile deleted successfully.", "Profile Deleted", + MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to delete profile:\n{ex.Message}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + #endregion + + #region File Browsing + + // Wire this to output_folder_browse_button.Click in the Designer + public void output_folder_browse_button_Click(object? sender, EventArgs e) + { + using var dialog = new SaveFileDialog + { + Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*", + Title = "Select Text File Output Path", + FileName = "now_playing.txt" + }; + + if (dialog.ShowDialog() == DialogResult.OK) + { + if (output_folder_text_box != null) + output_folder_text_box.Text = dialog.FileName; + } + } + + // Wire this to btnBrowseConfigFile.Click in the Designer + public void btnBrowseConfigFile_Click(object? sender, EventArgs e) + { + using var dialog = new SaveFileDialog + { + Filter = "Config Files (*.config)|*.config|All Files (*.*)|*.*", + Title = "Select Pianobar Config File Path", + FileName = "pianobar.config" + }; + + if (dialog.ShowDialog() == DialogResult.OK) + { + if (txtConfigFilePath != null) + txtConfigFilePath.Text = dialog.FileName; + } + } + + #endregion + + #region Credential Override + + // Wire this to chkOverrideCredentials.CheckedChanged in the Designer + public void chkOverrideCredentials_CheckedChanged(object? sender, EventArgs e) + { + if (sender is CheckBox chk) + ToggleCredentialFields(chk.Checked); + } + + private void ToggleCredentialFields(bool enabled) + { + if (txtConfigFilePath != null) + txtConfigFilePath.Enabled = enabled; + + if (btnBrowseConfigFile != null) + btnBrowseConfigFile.Enabled = enabled; + + if (txtPianobarUser != null) + txtPianobarUser.Enabled = enabled; + + if (txtPianobarPass != null) + txtPianobarPass.Enabled = enabled; + } + + #endregion + + #region Save/Cancel + + // Wire this to btnSave.Click in the Designer (or OK button) + public void btnSave_Click(object? sender, EventArgs e) + { + try + { + SaveSettingsFromControls(); + + if (!string.IsNullOrEmpty(_currentProfileName)) + { + _profileManager.UpdateProfile(_currentProfileName, _currentSettings); + } + + DialogResult = DialogResult.OK; + Close(); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to save settings:\n{ex.Message}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + // Wire this to btnCancel.Click in the Designer (or Cancel button) + public void btnCancel_Click(object? sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + Close(); + } + + #endregion + + #region Helper Methods + + private string? PromptForProfileName(string title, string prompt) + { + using var form = new Form + { + Text = title, + Width = 400, + Height = 150, + StartPosition = FormStartPosition.CenterParent, + FormBorderStyle = FormBorderStyle.FixedDialog, + MaximizeBox = false, + MinimizeBox = false + }; + + var label = new Label { Text = prompt, Left = 10, Top = 20, Width = 360 }; + var textBox = new TextBox { Left = 10, Top = 50, Width = 360 }; + var btnOk = new Button { Text = "OK", Left = 210, Top = 80, DialogResult = DialogResult.OK }; + var btnCancel = new Button { Text = "Cancel", Left = 290, Top = 80, DialogResult = DialogResult.Cancel }; + + form.Controls.AddRange(new Control[] { label, textBox, btnOk, btnCancel }); + form.AcceptButton = btnOk; + form.CancelButton = btnCancel; + + return form.ShowDialog() == DialogResult.OK ? textBox.Text : null; + } + + #endregion } } diff --git a/StationForm.Designer.cs b/StationForm.Designer.cs index da762fc..c17099b 100644 --- a/StationForm.Designer.cs +++ b/StationForm.Designer.cs @@ -65,6 +65,7 @@ textBox1.Name = "textBox1"; textBox1.Size = new Size(273, 24); textBox1.TabIndex = 1; + textBox1.TextChanged += textBox1_TextChanged; // // lstStations // @@ -91,6 +92,7 @@ btnSelectStation.TabIndex = 2; btnSelectStation.Text = "Refresh stations"; btnSelectStation.UseVisualStyleBackColor = true; + btnSelectStation.Click += btnSelectStation_Click; // // btnCancelStation // @@ -100,6 +102,7 @@ btnCancelStation.TabIndex = 3; btnCancelStation.Text = "Cancel"; btnCancelStation.UseVisualStyleBackColor = true; + btnCancelStation.Click += btnCancelStation_Click; // // StationForm // diff --git a/StationForm.cs b/StationForm.cs index 7136b57..fb34433 100644 --- a/StationForm.cs +++ b/StationForm.cs @@ -1,20 +1,220 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - namespace PainoBar_Helper { public partial class StationForm : Form { - public StationForm() + private readonly PianobarManager _pianobarManager; + private List _stations; + private List _filteredStations; + private bool _stationsLoaded = false; + + public StationForm(PianobarManager pianobarManager) { InitializeComponent(); + + _pianobarManager = pianobarManager; + _stations = new List(); + _filteredStations = new List(); + + InitializeControls(); + LoadStations(); } + + #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; + } + } + + private void LoadStations() + { + // Subscribe to output to capture station list + _pianobarManager.OutputReceived += PianobarManager_OutputReceived; + + // Send command to show stations + try + { + _pianobarManager.ShowStations(); + + // Show loading message + if (lstStations != null) + { + 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); + } + } + + #endregion + + #region Station Loading + + private void PianobarManager_OutputReceived(object? sender, string e) + { + // Parse station list from Pianobar output + // Format is typically: "0) Station Name" + var match = System.Text.RegularExpressions.Regex.Match(e, @"^\s*(\d+)\)\s*(.+)$"); + if (match.Success) + { + string stationName = match.Groups[2].Value.Trim(); + if (!_stations.Contains(stationName)) + { + _stations.Add(stationName); + Invoke(() => UpdateStationList()); + } + } + + // Detect end of station list + if (e.Contains("Select station:") || e.Contains("[?]")) + { + _stationsLoaded = true; + _pianobarManager.OutputReceived -= PianobarManager_OutputReceived; + Invoke(() => FinalizeStationList()); + } + } + + 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.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0) + .ToList(); + } + + UpdateStationList(); + } + + #endregion + + #region Station Selection + + // Wire this to btnSelectStation.Click in the Designer + public void btnSelectStation_Click(object? sender, EventArgs e) + { + SelectCurrentStation(); + } + + private void LstStations_DoubleClick(object? sender, EventArgs e) + { + SelectCurrentStation(); + } + + private void SelectCurrentStation() + { + if (lstStations == null) + return; + + if (lstStations.SelectedIndex < 0) + { + MessageBox.Show("Please select a station.", "No Selection", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + string selectedStation = lstStations.SelectedItem?.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(selectedStation)) + return; + + // Find the index in the original station list + int stationIndex = _stations.IndexOf(selectedStation); + if (stationIndex < 0) + return; + + try + { + // Send the station index to Pianobar + // Pianobar expects the station number (0-based index) + _pianobarManager.SendCommand(stationIndex.ToString()); + + DialogResult = DialogResult.OK; + Close(); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to select station:\n{ex.Message}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + #endregion + + #region Cancel + + // Wire this to btnCancelStation.Click in the Designer (or Cancel button) + public void btnCancelStation_Click(object? sender, EventArgs e) + { + // Send 'q' to cancel station selection in Pianobar + try + { + _pianobarManager.SendCommand('q'); + } + catch { } + + DialogResult = DialogResult.Cancel; + Close(); + } + + #endregion + + #region Cleanup + + protected override void OnFormClosing(FormClosingEventArgs e) + { + _pianobarManager.OutputReceived -= PianobarManager_OutputReceived; + base.OnFormClosing(e); + } + + #endregion } }