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();
|
connect_button = new Button();
|
||||||
disconnect_button = new Button();
|
disconnect_button = new Button();
|
||||||
statusStrip1 = new StatusStrip();
|
statusStrip1 = new StatusStrip();
|
||||||
|
statusLabelLeft = new ToolStripStatusLabel();
|
||||||
|
statusLabelCenter = new ToolStripStatusLabel();
|
||||||
|
statusLabelRight = new ToolStripStatusLabel();
|
||||||
stations_button = new Button();
|
stations_button = new Button();
|
||||||
groupBox1 = new GroupBox();
|
groupBox1 = new GroupBox();
|
||||||
btnPlayPause = new Button();
|
btnPlayPause = new Button();
|
||||||
btnSkip = new Button();
|
btnSkip = new Button();
|
||||||
menuStrip1.SuspendLayout();
|
menuStrip1.SuspendLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)album_art_picture_box).BeginInit();
|
((System.ComponentModel.ISupportInitialize)album_art_picture_box).BeginInit();
|
||||||
|
statusStrip1.SuspendLayout();
|
||||||
groupBox1.SuspendLayout();
|
groupBox1.SuspendLayout();
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
//
|
//
|
||||||
@@ -67,6 +71,7 @@
|
|||||||
settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
||||||
settingsToolStripMenuItem.Size = new Size(124, 22);
|
settingsToolStripMenuItem.Size = new Size(124, 22);
|
||||||
settingsToolStripMenuItem.Text = "Settings";
|
settingsToolStripMenuItem.Text = "Settings";
|
||||||
|
settingsToolStripMenuItem.Click += menuSettings_Click;
|
||||||
//
|
//
|
||||||
// profilesToolStripMenuItem
|
// profilesToolStripMenuItem
|
||||||
//
|
//
|
||||||
@@ -106,6 +111,7 @@
|
|||||||
connect_button.TabIndex = 4;
|
connect_button.TabIndex = 4;
|
||||||
connect_button.Text = "Connect";
|
connect_button.Text = "Connect";
|
||||||
connect_button.UseVisualStyleBackColor = true;
|
connect_button.UseVisualStyleBackColor = true;
|
||||||
|
connect_button.Click += btnConnect_Click;
|
||||||
//
|
//
|
||||||
// disconnect_button
|
// disconnect_button
|
||||||
//
|
//
|
||||||
@@ -115,15 +121,37 @@
|
|||||||
disconnect_button.TabIndex = 5;
|
disconnect_button.TabIndex = 5;
|
||||||
disconnect_button.Text = "Disconnect";
|
disconnect_button.Text = "Disconnect";
|
||||||
disconnect_button.UseVisualStyleBackColor = true;
|
disconnect_button.UseVisualStyleBackColor = true;
|
||||||
|
disconnect_button.Click += btnDisconnect_Click;
|
||||||
//
|
//
|
||||||
// statusStrip1
|
// statusStrip1
|
||||||
//
|
//
|
||||||
|
statusStrip1.Items.AddRange(new ToolStripItem[] { statusLabelLeft, statusLabelCenter, statusLabelRight });
|
||||||
statusStrip1.Location = new Point(0, 377);
|
statusStrip1.Location = new Point(0, 377);
|
||||||
statusStrip1.Name = "statusStrip1";
|
statusStrip1.Name = "statusStrip1";
|
||||||
statusStrip1.Size = new Size(666, 22);
|
statusStrip1.Size = new Size(666, 22);
|
||||||
statusStrip1.TabIndex = 6;
|
statusStrip1.TabIndex = 6;
|
||||||
statusStrip1.Text = "statusStrip1";
|
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
|
||||||
//
|
//
|
||||||
stations_button.Location = new Point(385, 253);
|
stations_button.Location = new Point(385, 253);
|
||||||
@@ -132,6 +160,7 @@
|
|||||||
stations_button.TabIndex = 7;
|
stations_button.TabIndex = 7;
|
||||||
stations_button.Text = "Stations";
|
stations_button.Text = "Stations";
|
||||||
stations_button.UseVisualStyleBackColor = true;
|
stations_button.UseVisualStyleBackColor = true;
|
||||||
|
stations_button.Click += btnStations_Click;
|
||||||
//
|
//
|
||||||
// groupBox1
|
// groupBox1
|
||||||
//
|
//
|
||||||
@@ -152,6 +181,7 @@
|
|||||||
btnPlayPause.TabIndex = 0;
|
btnPlayPause.TabIndex = 0;
|
||||||
btnPlayPause.Text = "Play / Pause";
|
btnPlayPause.Text = "Play / Pause";
|
||||||
btnPlayPause.UseVisualStyleBackColor = true;
|
btnPlayPause.UseVisualStyleBackColor = true;
|
||||||
|
btnPlayPause.Click += btnPlayPause_Click;
|
||||||
//
|
//
|
||||||
// btnSkip
|
// btnSkip
|
||||||
//
|
//
|
||||||
@@ -161,6 +191,7 @@
|
|||||||
btnSkip.TabIndex = 1;
|
btnSkip.TabIndex = 1;
|
||||||
btnSkip.Text = "Skip";
|
btnSkip.Text = "Skip";
|
||||||
btnSkip.UseVisualStyleBackColor = true;
|
btnSkip.UseVisualStyleBackColor = true;
|
||||||
|
btnSkip.Click += btnSkip_Click;
|
||||||
//
|
//
|
||||||
// Main
|
// Main
|
||||||
//
|
//
|
||||||
@@ -181,6 +212,8 @@
|
|||||||
menuStrip1.ResumeLayout(false);
|
menuStrip1.ResumeLayout(false);
|
||||||
menuStrip1.PerformLayout();
|
menuStrip1.PerformLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)album_art_picture_box).EndInit();
|
((System.ComponentModel.ISupportInitialize)album_art_picture_box).EndInit();
|
||||||
|
statusStrip1.ResumeLayout(false);
|
||||||
|
statusStrip1.PerformLayout();
|
||||||
groupBox1.ResumeLayout(false);
|
groupBox1.ResumeLayout(false);
|
||||||
ResumeLayout(false);
|
ResumeLayout(false);
|
||||||
PerformLayout();
|
PerformLayout();
|
||||||
@@ -201,5 +234,8 @@
|
|||||||
private GroupBox groupBox1;
|
private GroupBox groupBox1;
|
||||||
private Button btnPlayPause;
|
private Button btnPlayPause;
|
||||||
private Button btnSkip;
|
private Button btnSkip;
|
||||||
|
private ToolStripStatusLabel statusLabelLeft;
|
||||||
|
private ToolStripStatusLabel statusLabelCenter;
|
||||||
|
private ToolStripStatusLabel statusLabelRight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,404 @@ namespace PainoBar_Helper
|
|||||||
{
|
{
|
||||||
public partial class Main : Form
|
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()
|
public Main()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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
|
namespace PainoBar_Helper
|
||||||
{
|
{
|
||||||
partial class settings_form
|
partial class SettingsForm
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required designer variable.
|
/// Required designer variable.
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
profiles_combo_box_selector.Name = "profiles_combo_box_selector";
|
profiles_combo_box_selector.Name = "profiles_combo_box_selector";
|
||||||
profiles_combo_box_selector.Size = new Size(199, 25);
|
profiles_combo_box_selector.Size = new Size(199, 25);
|
||||||
profiles_combo_box_selector.TabIndex = 1;
|
profiles_combo_box_selector.TabIndex = 1;
|
||||||
|
profiles_combo_box_selector.SelectedIndexChanged += profiles_combo_box_selector_SelectedIndexChanged;
|
||||||
//
|
//
|
||||||
// new_profile_button
|
// new_profile_button
|
||||||
//
|
//
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
new_profile_button.TabIndex = 2;
|
new_profile_button.TabIndex = 2;
|
||||||
new_profile_button.Text = "+ New";
|
new_profile_button.Text = "+ New";
|
||||||
new_profile_button.UseVisualStyleBackColor = true;
|
new_profile_button.UseVisualStyleBackColor = true;
|
||||||
|
new_profile_button.Click += new_profile_button_Click;
|
||||||
//
|
//
|
||||||
// delete_profile_button
|
// delete_profile_button
|
||||||
//
|
//
|
||||||
@@ -113,6 +115,7 @@
|
|||||||
delete_profile_button.TabIndex = 3;
|
delete_profile_button.TabIndex = 3;
|
||||||
delete_profile_button.Text = "Delete";
|
delete_profile_button.Text = "Delete";
|
||||||
delete_profile_button.UseVisualStyleBackColor = true;
|
delete_profile_button.UseVisualStyleBackColor = true;
|
||||||
|
delete_profile_button.Click += delete_profile_button_Click;
|
||||||
//
|
//
|
||||||
// profile_name_label
|
// profile_name_label
|
||||||
//
|
//
|
||||||
@@ -251,6 +254,7 @@
|
|||||||
output_folder_browse_button.TabIndex = 3;
|
output_folder_browse_button.TabIndex = 3;
|
||||||
output_folder_browse_button.Text = "Browse";
|
output_folder_browse_button.Text = "Browse";
|
||||||
output_folder_browse_button.UseVisualStyleBackColor = true;
|
output_folder_browse_button.UseVisualStyleBackColor = true;
|
||||||
|
output_folder_browse_button.Click += output_folder_browse_button_Click;
|
||||||
//
|
//
|
||||||
// local_web_server_check_box
|
// local_web_server_check_box
|
||||||
//
|
//
|
||||||
@@ -307,6 +311,7 @@
|
|||||||
chkOverrideCredentials.TabIndex = 0;
|
chkOverrideCredentials.TabIndex = 0;
|
||||||
chkOverrideCredentials.Text = "Enable Profile Credentials and Custom Config Generation";
|
chkOverrideCredentials.Text = "Enable Profile Credentials and Custom Config Generation";
|
||||||
chkOverrideCredentials.UseVisualStyleBackColor = true;
|
chkOverrideCredentials.UseVisualStyleBackColor = true;
|
||||||
|
chkOverrideCredentials.CheckedChanged += chkOverrideCredentials_CheckedChanged;
|
||||||
//
|
//
|
||||||
// txtConfigFilePathLabel
|
// txtConfigFilePathLabel
|
||||||
//
|
//
|
||||||
@@ -332,6 +337,7 @@
|
|||||||
btnBrowseConfigFile.TabIndex = 3;
|
btnBrowseConfigFile.TabIndex = 3;
|
||||||
btnBrowseConfigFile.Text = "Browse";
|
btnBrowseConfigFile.Text = "Browse";
|
||||||
btnBrowseConfigFile.UseVisualStyleBackColor = true;
|
btnBrowseConfigFile.UseVisualStyleBackColor = true;
|
||||||
|
btnBrowseConfigFile.Click += btnBrowseConfigFile_Click;
|
||||||
//
|
//
|
||||||
// UsernameLabel
|
// UsernameLabel
|
||||||
//
|
//
|
||||||
@@ -366,14 +372,14 @@
|
|||||||
txtPianobarPass.TabIndex = 7;
|
txtPianobarPass.TabIndex = 7;
|
||||||
txtPianobarPass.UseSystemPasswordChar = true;
|
txtPianobarPass.UseSystemPasswordChar = true;
|
||||||
//
|
//
|
||||||
// settings_form
|
// SettingsForm
|
||||||
//
|
//
|
||||||
AutoScaleDimensions = new SizeF(7F, 17F);
|
AutoScaleDimensions = new SizeF(7F, 17F);
|
||||||
AutoScaleMode = AutoScaleMode.Font;
|
AutoScaleMode = AutoScaleMode.Font;
|
||||||
ClientSize = new Size(454, 586);
|
ClientSize = new Size(454, 586);
|
||||||
Controls.Add(profile_configuration_group_box);
|
Controls.Add(profile_configuration_group_box);
|
||||||
Controls.Add(profiles_group_box);
|
Controls.Add(profiles_group_box);
|
||||||
Name = "settings_form";
|
Name = "SettingsForm";
|
||||||
Text = "Settings";
|
Text = "Settings";
|
||||||
profiles_group_box.ResumeLayout(false);
|
profiles_group_box.ResumeLayout(false);
|
||||||
profiles_group_box.PerformLayout();
|
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
|
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();
|
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.Name = "textBox1";
|
||||||
textBox1.Size = new Size(273, 24);
|
textBox1.Size = new Size(273, 24);
|
||||||
textBox1.TabIndex = 1;
|
textBox1.TabIndex = 1;
|
||||||
|
textBox1.TextChanged += textBox1_TextChanged;
|
||||||
//
|
//
|
||||||
// lstStations
|
// lstStations
|
||||||
//
|
//
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
btnSelectStation.TabIndex = 2;
|
btnSelectStation.TabIndex = 2;
|
||||||
btnSelectStation.Text = "Refresh stations";
|
btnSelectStation.Text = "Refresh stations";
|
||||||
btnSelectStation.UseVisualStyleBackColor = true;
|
btnSelectStation.UseVisualStyleBackColor = true;
|
||||||
|
btnSelectStation.Click += btnSelectStation_Click;
|
||||||
//
|
//
|
||||||
// btnCancelStation
|
// btnCancelStation
|
||||||
//
|
//
|
||||||
@@ -100,6 +102,7 @@
|
|||||||
btnCancelStation.TabIndex = 3;
|
btnCancelStation.TabIndex = 3;
|
||||||
btnCancelStation.Text = "Cancel";
|
btnCancelStation.Text = "Cancel";
|
||||||
btnCancelStation.UseVisualStyleBackColor = true;
|
btnCancelStation.UseVisualStyleBackColor = true;
|
||||||
|
btnCancelStation.Click += btnCancelStation_Click;
|
||||||
//
|
//
|
||||||
// StationForm
|
// 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
|
namespace PainoBar_Helper
|
||||||
{
|
{
|
||||||
public partial class StationForm : Form
|
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();
|
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