first test
This commit is contained in:
@@ -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:<port>/`
|
||||
- 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.
|
||||
Generated
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PainoBar_Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages various output mechanisms for song metadata
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PainoBar_Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the Pianobar process lifecycle and communication
|
||||
/// </summary>
|
||||
public class PianobarManager : IDisposable
|
||||
{
|
||||
private Process? _process;
|
||||
private StreamWriter? _inputWriter;
|
||||
private bool _isConnected;
|
||||
private readonly object _lockObject = new object();
|
||||
|
||||
public event EventHandler<SongChangedEventArgs>? SongChanged;
|
||||
public event EventHandler<string>? StationChanged;
|
||||
public event EventHandler<PianobarEventArgs>? EventOccurred;
|
||||
public event EventHandler? Connected;
|
||||
public event EventHandler? Disconnected;
|
||||
public event EventHandler<string>? 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PainoBar_Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages Pianobar profiles including creation, deletion, and configuration file generation
|
||||
/// </summary>
|
||||
public class ProfileManager
|
||||
{
|
||||
private const string ProfilesFileName = "profiles.json";
|
||||
private readonly string _profilesFilePath;
|
||||
private List<Profile> _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<Profile>();
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
public event EventHandler? ProfilesChanged;
|
||||
public event EventHandler? ActiveProfileChanged;
|
||||
|
||||
public IReadOnlyList<Profile> 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<Profile>();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(_profilesFilePath);
|
||||
var data = JsonSerializer.Deserialize<ProfileData>(json);
|
||||
if (data != null)
|
||||
{
|
||||
_profiles = data.Profiles ?? new List<Profile>();
|
||||
_activeProfileName = data.ActiveProfile;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_profiles = new List<Profile>();
|
||||
}
|
||||
}
|
||||
|
||||
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<Profile>? Profiles { get; set; }
|
||||
public string? ActiveProfile { get; set; }
|
||||
}
|
||||
}
|
||||
Generated
+9
-3
@@ -1,6 +1,6 @@
|
||||
namespace PainoBar_Helper
|
||||
{
|
||||
partial class settings_form
|
||||
partial class SettingsForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 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();
|
||||
|
||||
+344
-12
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
@@ -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
|
||||
//
|
||||
|
||||
+211
-11
@@ -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<string> _stations;
|
||||
private List<string> _filteredStations;
|
||||
private bool _stationsLoaded = false;
|
||||
|
||||
public StationForm(PianobarManager pianobarManager)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_pianobarManager = pianobarManager;
|
||||
_stations = new List<string>();
|
||||
_filteredStations = new List<string>();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user