Files
RadioDJViewer/RadioDJViewer/Main.cs

794 lines
31 KiB
C#

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;
using System.Xml;
using System.Threading;
using System.IO;
using System.Resources;
namespace RadioDJViewer
{
public partial class Main : Form
{
private string _currentTrackKey = string.Empty;
private string selectedFolderPath = string.Empty;
private string outputFolderPath = string.Empty;
private bool isConnected = false;
private string currentProfile = "Default";
private string currentSongImagePath = string.Empty; // Path to the current song image
private string defaultImagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "default.jpg");
private string mainImagesFolderPath = string.Empty; // New field for main images folder path
private Profile loadedProfile = null; // Field to store the loaded profile
private WidgetServer widgetServer = null; // Local widget HTTP server
private bool widgetRunning = false;
private CancellationTokenSource statusCts = null;
// Timer to poll REST API
private System.Windows.Forms.Timer apiTimer;
private string lastApiXml = null;
private System.Windows.Forms.Timer marqueeTimerTitle;
private System.Windows.Forms.Timer marqueeTimerArtist;
private System.Windows.Forms.Timer marqueeTimerAlbum;
private int marqueeOffsetTitle = 0;
private int marqueeOffsetArtist = 0;
private int marqueeOffsetAlbum = 0;
private string marqueeTextTitle = "";
private string marqueeTextArtist = "";
private string marqueeTextAlbum = "";
private System.Windows.Forms.Timer pauseTimerTitle;
private System.Windows.Forms.Timer pauseTimerArtist;
private System.Windows.Forms.Timer pauseTimerAlbum;
private int marqueeScrollSpeed = 100; // Default to Medium
private int marqueePauseTime = 6000; // Default to 6 seconds
private string marqueeSeparator = " | "; // Default separator
private bool loggingEnabled = false;
private string logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RadioDJViewer.log");
private bool isDarkMode = false; // New field for dark mode
public Main()
{
InitializeComponent();
UpdateStatusBar();
// Set up timer for polling API every 3 seconds
apiTimer = new System.Windows.Forms.Timer();
apiTimer.Interval = 3000; // 3 seconds
apiTimer.Tick += ApiTimer_Tick;
// Marquee timers for each label
marqueeTimerTitle = new System.Windows.Forms.Timer();
marqueeTimerTitle.Interval = marqueeScrollSpeed;
marqueeTimerTitle.Tick += MarqueeTimerTitle_Tick;
marqueeTimerArtist = new System.Windows.Forms.Timer();
marqueeTimerArtist.Interval = marqueeScrollSpeed;
marqueeTimerArtist.Tick += MarqueeTimerArtist_Tick;
marqueeTimerAlbum = new System.Windows.Forms.Timer();
marqueeTimerAlbum.Interval = marqueeScrollSpeed;
marqueeTimerAlbum.Tick += MarqueeTimerAlbum_Tick;
// Wire up dark mode button
this.uicolor.Click += Uicolor_Click;
this.load_profile_button.Click += load_profile_button_Click;
PopulateProfileDropBox();
UpdateTheme();
// Auto-load profiles if profiles.json exists
AutoLoadProfiles();
}
private void EnsureDefaultImageExists()
{
if (!File.Exists(defaultImagePath))
{
// Try to extract from resources
var fallbackImg = Properties.Resources.fallback;
if (fallbackImg != null)
{
fallbackImg.Save(defaultImagePath, System.Drawing.Imaging.ImageFormat.Jpeg);
}
}
}
public void SetMarqueeScrollSpeed(string speed)
{
switch (speed)
{
case "Very Slow": marqueeScrollSpeed = 300; break;
case "Slow": marqueeScrollSpeed = 200; break;
case "Medium": marqueeScrollSpeed = 100; break;
case "Fast": marqueeScrollSpeed = 60; break;
case "Very Fast": marqueeScrollSpeed = 30; break;
default: marqueeScrollSpeed = 100; break;
}
marqueeTimerTitle.Interval = marqueeScrollSpeed;
marqueeTimerArtist.Interval = marqueeScrollSpeed;
marqueeTimerAlbum.Interval = marqueeScrollSpeed;
}
public void SetMarqueePauseTime(int seconds)
{
marqueePauseTime = seconds * 1000;
}
public void SetMarqueeSeparator(string separator)
{
marqueeSeparator = separator;
}
private void LogMessage(string message)
{
if (!loggingEnabled) return;
try
{
File.AppendAllText(logFilePath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}\r\n");
}
catch { }
}
private int GetVisibleChars(Label label)
{
// Use the label's actual width and font to estimate visible characters
using (Graphics g = label.CreateGraphics())
{
// Use the full label width for measurement
SizeF size = g.MeasureString("W", label.Font);
int chars = (int)(label.Width / size.Width);
// If the label is single-line, ensure we use the full width
return Math.Max(chars, 1);
}
}
private void SetLabelTextFull(Label label, string text)
{
// Always set the full text, and let marquee logic handle scrolling
label.Text = text;
}
private void MarqueeTimerTitle_Tick(object sender, EventArgs e)
{
int visibleChars = GetVisibleChars(label4);
if (marqueeTextTitle.Length > visibleChars)
{
string separator = string.IsNullOrEmpty(marqueeSeparator) ? "" : marqueeSeparator;
string scrollText = marqueeTextTitle + separator;
string repeatedScrollText = scrollText + scrollText;
marqueeOffsetTitle = (marqueeOffsetTitle + 1) % scrollText.Length;
label4.Text = repeatedScrollText.Substring(marqueeOffsetTitle, visibleChars);
LogMessage($"MarqueeTitle: separator='{separator}', scrollText='{scrollText}', offset={marqueeOffsetTitle}");
if (marqueeOffsetTitle == 0)
{
marqueeTimerTitle.Stop();
if (pauseTimerTitle == null)
{
pauseTimerTitle = new System.Windows.Forms.Timer();
pauseTimerTitle.Tick += (s, args) => {
pauseTimerTitle.Stop();
marqueeTimerTitle.Start();
};
}
pauseTimerTitle.Interval = marqueePauseTime;
pauseTimerTitle.Start();
}
}
else
{
SetLabelTextFull(label4, marqueeTextTitle);
}
}
private void MarqueeTimerArtist_Tick(object sender, EventArgs e)
{
int visibleChars = GetVisibleChars(label5);
if (marqueeTextArtist.Length > visibleChars)
{
string separator = string.IsNullOrEmpty(marqueeSeparator) ? "" : marqueeSeparator;
string scrollText = marqueeTextArtist + separator;
string repeatedScrollText = scrollText + scrollText;
marqueeOffsetArtist = (marqueeOffsetArtist + 1) % scrollText.Length;
label5.Text = repeatedScrollText.Substring(marqueeOffsetArtist, visibleChars);
LogMessage($"MarqueeArtist: separator='{separator}', scrollText='{scrollText}', offset={marqueeOffsetArtist}");
if (marqueeOffsetArtist == 0)
{
marqueeTimerArtist.Stop();
if (pauseTimerArtist == null)
{
pauseTimerArtist = new System.Windows.Forms.Timer();
pauseTimerArtist.Tick += (s, args) => {
pauseTimerArtist.Stop();
marqueeTimerArtist.Start();
};
}
pauseTimerArtist.Interval = marqueePauseTime;
pauseTimerArtist.Start();
}
}
else
{
SetLabelTextFull(label5, marqueeTextArtist);
}
}
private void MarqueeTimerAlbum_Tick(object sender, EventArgs e)
{
int visibleChars = GetVisibleChars(label6);
if (marqueeTextAlbum.Length > visibleChars)
{
string separator = string.IsNullOrEmpty(marqueeSeparator) ? "" : marqueeSeparator;
string scrollText = marqueeTextAlbum + separator;
string repeatedScrollText = scrollText + scrollText;
marqueeOffsetAlbum = (marqueeOffsetAlbum + 1) % scrollText.Length;
label6.Text = repeatedScrollText.Substring(marqueeOffsetAlbum, visibleChars);
LogMessage($"MarqueeAlbum: separator='{separator}', scrollText='{scrollText}', offset={marqueeOffsetAlbum}");
if (marqueeOffsetAlbum == 0)
{
marqueeTimerAlbum.Stop();
if (pauseTimerAlbum == null)
{
pauseTimerAlbum = new System.Windows.Forms.Timer();
pauseTimerAlbum.Tick += (s, args) => {
pauseTimerAlbum.Stop();
marqueeTimerAlbum.Start();
};
}
pauseTimerAlbum.Interval = marqueePauseTime;
pauseTimerAlbum.Start();
}
}
else
{
SetLabelTextFull(label6, marqueeTextAlbum);
}
}
private void ApiTimer_Tick(object sender, EventArgs e)
{
if (isConnected)
{
_ = PollRestApi();
}
}
private async Task PollRestApi()
{
if (loadedProfile == null) return;
string url = loadedProfile.UrlFormat;
if (!string.IsNullOrWhiteSpace(url))
{
url = url.Replace("{ip}", loadedProfile.IP ?? "")
.Replace("{port}", loadedProfile.Port ?? "")
.Replace("{password}", loadedProfile.Password ?? "");
}
else
{
return;
}
try
{
var client = new System.Net.Http.HttpClient();
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var xml = await response.Content.ReadAsStringAsync();
if (xml != lastApiXml)
{
lastApiXml = xml;
ParseAndDisplaySongInfo(xml);
// Build a key from Artist and Title and only show change if different
try
{
var newKey = $"{marqueeTextArtist}|{marqueeTextTitle}";
if (!string.Equals(newKey, _currentTrackKey, StringComparison.Ordinal))
{
_currentTrackKey = newKey;
ShowTemporaryStatus("Song Change Detected", 2000);
}
}
catch { }
}
}
}
catch { }
}
private void AutoLoadProfiles()
{
string profilesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "profiles.json");
if (File.Exists(profilesPath))
{
var profileNames = ProfileStorage.GetProfileNames();
if (profileNames.Count > 0)
{
// Optionally, load the first profile by default
var profile = ProfileStorage.LoadProfile(profileNames[0]);
if (profile != null)
{
LoadProfile(profile);
}
}
}
}
private async void button1_Click(object sender, EventArgs e)
{
// Show connecting message
ShowTemporaryStatus("Connecting to API...", 0);
// Connect to REST API (ConnectToRestApi will start web server if needed)
await ConnectToRestApi();
if (isConnected)
{
apiTimer.Start();
}
}
private void button2_Click(object sender, EventArgs e)
{
// Disconnect
isConnected = false;
apiTimer.Stop();
// Stop widget server when disconnecting
try
{
widgetServer?.Stop();
widgetServer = null;
widgetRunning = false;
}
catch { }
ClearStatusMessage();
UpdateStatusBar();
}
private async Task ConnectToRestApi()
{
// Use loaded profile values
if (loadedProfile == null)
{
MessageBox.Show("No profile loaded. Please load a profile first.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
isConnected = false;
UpdateStatusBar();
return;
}
string url = loadedProfile.UrlFormat;
if (!string.IsNullOrWhiteSpace(url))
{
url = url.Replace("{ip}", loadedProfile.IP ?? "")
.Replace("{port}", loadedProfile.Port ?? "")
.Replace("{password}", loadedProfile.Password ?? "");
}
else
{
MessageBox.Show("Profile URL format is missing.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
isConnected = false;
UpdateStatusBar();
return;
}
try
{
var client = new System.Net.Http.HttpClient();
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var xml = await response.Content.ReadAsStringAsync();
ParseAndDisplaySongInfo(xml);
try
{
_currentTrackKey = $"{marqueeTextArtist}|{marqueeTextTitle}";
}
catch { }
isConnected = true;
// Clear the 'Connecting...' persistent message and show connected
ClearStatusMessage();
UpdateStatusBar();
// Start web server immediately after API connection if profile requests it
StartWidgetServerIfEnabled();
}
else
{
isConnected = false;
}
}
catch (Exception ex)
{
MessageBox.Show($"Error connecting to REST API: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
isConnected = false;
}
UpdateStatusBar();
}
// Call this method after parsing song info, assuming image path is available from profile or API
private void UpdateCurrentSongImage(string imageName)
{
string imagePath = string.Empty;
string outputImageName = loadedProfile?.OutputImageName;
bool useFallback = false;
if (!string.IsNullOrEmpty(mainImagesFolderPath) && !string.IsNullOrEmpty(imageName))
{
imagePath = Path.Combine(mainImagesFolderPath, imageName);
if (!File.Exists(imagePath))
{
useFallback = true;
}
}
else
{
useFallback = true;
}
if (useFallback)
{
// Use fallback PNG from resources
var fallbackImg = Properties.Resources.fallback;
if (fallbackImg != null)
{
var resized = new Bitmap(fallbackImg, pictureBox1.Size);
pictureBox1.Image = resized;
// Save fallback image to output folder as PNG
if (!string.IsNullOrEmpty(outputFolderPath) && !string.IsNullOrEmpty(outputImageName))
{
string destPath = Path.Combine(outputFolderPath, Path.ChangeExtension(outputImageName, ".png"));
resized.Save(destPath, System.Drawing.Imaging.ImageFormat.Png);
}
}
else
{
pictureBox1.Image = null;
}
return;
}
try
{
using (var img = Image.FromFile(imagePath))
{
var resized = new Bitmap(img, pictureBox1.Size);
pictureBox1.Image = resized;
// Save to output folder as PNG
if (!string.IsNullOrEmpty(outputFolderPath) && !string.IsNullOrEmpty(outputImageName))
{
string destPath = Path.Combine(outputFolderPath, Path.ChangeExtension(outputImageName, ".png"));
resized.Save(destPath, System.Drawing.Imaging.ImageFormat.Png);
}
}
}
catch
{
// If loading image fails, fallback
var fallbackImg = Properties.Resources.fallback;
if (fallbackImg != null)
{
var resized = new Bitmap(fallbackImg, pictureBox1.Size);
pictureBox1.Image = resized;
if (!string.IsNullOrEmpty(outputFolderPath) && !string.IsNullOrEmpty(outputImageName))
{
string destPath = Path.Combine(outputFolderPath, Path.ChangeExtension(outputImageName, ".png"));
resized.Save(destPath, System.Drawing.Imaging.ImageFormat.Png);
}
}
else
{
pictureBox1.Image = null;
}
}
}
private void StartMarqueeTimers()
{
marqueeOffsetTitle = 0;
marqueeOffsetArtist = 0;
marqueeOffsetAlbum = 0;
marqueeTimerTitle.Start();
marqueeTimerArtist.Start();
marqueeTimerAlbum.Start();
}
private void StopMarqueeTimers()
{
marqueeTimerTitle.Stop();
marqueeTimerArtist.Stop();
marqueeTimerAlbum.Stop();
}
private void ParseAndDisplaySongInfo(string xml)
{
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
// Parse title, artist, album from XML (case-insensitive)
string title = doc.SelectSingleNode("//*[translate(local-name(), 'TITLE', 'title')='title']")?.InnerText;
string artist = doc.SelectSingleNode("//*[translate(local-name(), 'ARTIST', 'artist')='artist']")?.InnerText;
string album = doc.SelectSingleNode("//*[translate(local-name(), 'ALBUM', 'album')='album']")?.InnerText;
string albumArt = doc.SelectSingleNode("//*[translate(local-name(), 'ALBUMART', 'albumart')='albumart']")?.InnerText;
marqueeTextTitle = !string.IsNullOrWhiteSpace(title) ? title : "No title";
marqueeTextArtist = !string.IsNullOrWhiteSpace(artist) ? artist : "No artist";
marqueeTextAlbum = !string.IsNullOrWhiteSpace(album) ? album : "No album";
StartMarqueeTimers();
// Write to output files
if (!string.IsNullOrEmpty(outputFolderPath))
{
File.WriteAllText(Path.Combine(outputFolderPath, "title.txt"), marqueeTextTitle);
File.WriteAllText(Path.Combine(outputFolderPath, "artist.txt"), marqueeTextArtist);
File.WriteAllText(Path.Combine(outputFolderPath, "album.txt"), marqueeTextAlbum);
}
// Handle album art image
if (!string.IsNullOrWhiteSpace(albumArt))
{
UpdateCurrentSongImage(albumArt);
}
else
{
UpdateCurrentSongImage(null);
}
}
catch
{
marqueeTextTitle = "No title";
marqueeTextArtist = "No artist";
marqueeTextAlbum = "No album";
StartMarqueeTimers();
pictureBox1.Image = null;
}
}
private void UpdateStatusBar()
{
// Update color box based on connection states
try
{
if (!isConnected)
{
toolStripStatusLabel2.BackColor = Color.Red; // disconnected/error
}
else
{
if (isConnected && widgetRunning)
toolStripStatusLabel2.BackColor = Color.Blue; // both running
else
toolStripStatusLabel2.BackColor = Color.Green; // API ok, web off
}
}
catch { }
// Profile
toolStripStatusLabel4.Text = $"Profile: {currentProfile}";
// If there's no active temporary message, ensure label3 shows default
if (statusCts == null)
{
toolStripStatusLabel3.Text = isConnected ? "Connected to API" : "Disconnected";
}
}
private void ShowTemporaryStatus(string message, int milliseconds)
{
// Cancel any previous temporary status
try
{
statusCts?.Cancel();
}
catch { }
statusCts = null;
if (milliseconds <= 0)
{
// Persistent message until changed - keep a non-null token so UpdateStatusBar doesn't overwrite
var persistent = new CancellationTokenSource();
statusCts = persistent;
toolStripStatusLabel3.Text = message;
return;
}
var cts = new CancellationTokenSource();
statusCts = cts;
toolStripStatusLabel3.Text = message;
Task.Run(async () =>
{
try
{
await Task.Delay(milliseconds, cts.Token);
if (!cts.IsCancellationRequested)
{
// clear temporary message
statusCts = null;
this.BeginInvoke((Action)(() => {
toolStripStatusLabel3.Text = isConnected ? "Connected to API" : "Disconnected";
}));
}
}
catch { }
});
}
private void ClearStatusMessage()
{
try { statusCts?.Cancel(); } catch { }
statusCts = null;
try { toolStripStatusLabel3.Text = isConnected ? "Connected to API" : "Disconnected"; } catch { }
}
private void StartWidgetServerIfEnabled()
{
// Called after API connection is confirmed
if (loadedProfile == null || !loadedProfile.WebServerEnabled)
return;
try
{
if (widgetServer == null)
{
int port = loadedProfile.WebServerPort > 0 ? loadedProfile.WebServerPort : 8080;
widgetServer = new WidgetServer(port, () => marqueeTextArtist, () => marqueeTextTitle, () => this.currentSongImagePath ?? this.defaultImagePath);
}
widgetServer.Start();
widgetRunning = true;
UpdateStatusBar();
ShowTemporaryStatus($"Web: Running on {loadedProfile.WebServerPort}", 5000);
}
catch (Exception ex)
{
widgetRunning = false;
UpdateStatusBar();
// Show error message
ShowTemporaryStatus($"Web server error: {ex.Message}", 5000);
}
}
private void restAPISettingToolStripMenuItem_Click(object sender, EventArgs e)
{
// Open settings/profile dialog
var settingsForm = new Form1();
settingsForm.ApplyTheme(isDarkMode); // Pass current color mode
settingsForm.ShowDialog();
}
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
{
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
var versionAttr = assembly.GetCustomAttributes(typeof(System.Reflection.AssemblyFileVersionAttribute), false)
.OfType<System.Reflection.AssemblyFileVersionAttribute>().FirstOrDefault();
string version = versionAttr != null ? versionAttr.Version : assembly.GetName().Version.ToString();
string message = $"RadioDJViewer\nVersion: {version}\nGitHub: https://git.smartcraft.me/minster586/RadioDJViewer";
MessageBox.Show(message, "About RadioDJViewer", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
Application.Exit();
}
private void toolStripStatusLabel4_Click(object sender, EventArgs e)
{
// Profile click handler (to be implemented)
}
public void LoadProfile(Profile profile)
{
// Update main form fields from profile
currentProfile = profile.Name;
selectedFolderPath = profile.OutputFolder;
outputFolderPath = profile.OutputFolder;
mainImagesFolderPath = profile.MainImagesFolder; // Update new field
loadedProfile = profile; // Store loaded profile for REST API connection
// Update polling rate when profile is loaded
int pollingRate = profile.PollingRateSeconds > 0 ? profile.PollingRateSeconds : 3;
apiTimer.Interval = pollingRate * 1000;
SetMarqueeScrollSpeed(profile.MarqueeScrollSpeed ?? "Medium");
SetMarqueePauseTime(profile.MarqueePauseTime > 0 ? profile.MarqueePauseTime : 6);
SetMarqueeSeparator(profile.MarqueeSeparator ?? " | ");
// Immediately apply marquee settings to timers
if (pauseTimerTitle != null) pauseTimerTitle.Interval = marqueePauseTime;
if (pauseTimerArtist != null) pauseTimerArtist.Interval = marqueePauseTime;
if (pauseTimerAlbum != null) pauseTimerAlbum.Interval = marqueePauseTime;
marqueeTimerTitle.Interval = marqueeScrollSpeed;
marqueeTimerArtist.Interval = marqueeScrollSpeed;
marqueeTimerAlbum.Interval = marqueeScrollSpeed;
UpdateStatusBar();
// You can add more logic to update other fields if needed
}
private void Uicolor_Click(object sender, EventArgs e)
{
isDarkMode = !isDarkMode;
UpdateTheme();
}
private Bitmap InvertIcon(Bitmap original)
{
Bitmap inverted = new Bitmap(original.Width, original.Height);
for (int y = 0; y < original.Height; y++)
{
for (int x = 0; x < original.Width; x++)
{
Color pixel = original.GetPixel(x, y);
Color inv = Color.FromArgb(pixel.A, 255 - pixel.R, 255 - pixel.G, 255 - pixel.B);
inverted.SetPixel(x, y, inv);
}
}
return inverted;
}
private void UpdateTheme()
{
// Set icon
Bitmap icon = isDarkMode ? Properties.Resources.MdiWeatherSunny : Properties.Resources.MdiWeatherNight;
// Invert sun icon for visibility in dark mode
if (isDarkMode && icon != null)
{
icon = InvertIcon(icon);
}
// Resize icon to fit button and center it
if (icon != null)
{
int btnW = uicolor.Width;
int btnH = uicolor.Height;
var resizedIcon = new Bitmap(icon, btnW - 8, btnH - 8); // Padding for centering
uicolor.Image = resizedIcon;
uicolor.ImageAlign = ContentAlignment.MiddleCenter;
}
// Set colors
Color backColor = isDarkMode ? Color.FromArgb(32, 32, 32) : SystemColors.Control;
Color foreColor = isDarkMode ? Color.White : SystemColors.ControlText;
this.BackColor = backColor;
this.ForeColor = foreColor;
foreach (Control c in this.Controls)
{
ApplyThemeRecursive(c, backColor, foreColor);
}
}
private void ApplyThemeRecursive(Control control, Color backColor, Color foreColor)
{
control.BackColor = backColor;
control.ForeColor = foreColor;
foreach (Control child in control.Controls)
{
ApplyThemeRecursive(child, backColor, foreColor);
}
}
private void label7_Click(object sender, EventArgs e)
{
}
public string GetCurrentProfileName()
{
return currentProfile;
}
private void PopulateProfileDropBox()
{
profile_drop_box.Items.Clear();
var profileNames = ProfileStorage.GetProfileNames();
profile_drop_box.Items.AddRange(profileNames.ToArray());
// Set selected item to current profile if available
string currentProfile = GetCurrentProfileName();
if (!string.IsNullOrEmpty(currentProfile) && profileNames.Contains(currentProfile))
profile_drop_box.SelectedItem = currentProfile;
else if (profile_drop_box.Items.Count > 0)
profile_drop_box.SelectedIndex = 0;
}
private void load_profile_button_Click(object sender, EventArgs e)
{
if (profile_drop_box.SelectedItem != null)
{
var profile = ProfileStorage.LoadProfile(profile_drop_box.SelectedItem.ToString());
if (profile != null)
{
LoadProfile(profile);
UpdateStatusBar();
MessageBox.Show($"Profile '{profile.Name}' loaded.", "Profile Loaded", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
}
}