diff --git a/RadioDJViewer/Main.Designer.cs b/RadioDJViewer/Main.Designer.cs index 8912054..08bccda 100644 --- a/RadioDJViewer/Main.Designer.cs +++ b/RadioDJViewer/Main.Designer.cs @@ -43,6 +43,8 @@ this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.label8 = new System.Windows.Forms.Label(); + this.label7 = new System.Windows.Forms.Label(); this.label6 = new System.Windows.Forms.Label(); this.label5 = new System.Windows.Forms.Label(); this.label4 = new System.Windows.Forms.Label(); @@ -56,8 +58,9 @@ this.profile_drop_box = new System.Windows.Forms.ComboBox(); this.profile_label = new System.Windows.Forms.Label(); this.load_profile_button = new System.Windows.Forms.Button(); - this.label7 = new System.Windows.Forms.Label(); - this.label8 = new System.Windows.Forms.Label(); + this.time_label = new System.Windows.Forms.Label(); + this.label9 = new System.Windows.Forms.Label(); + this.time_data_label = new System.Windows.Forms.Label(); this.statusStrip1.SuspendLayout(); this.menuStrip2.SuspendLayout(); this.groupBox1.SuspendLayout(); @@ -71,9 +74,9 @@ this.toolStripStatusLabel2, this.toolStripStatusLabel4, this.toolStripStatusLabel3}); - this.statusStrip1.Location = new System.Drawing.Point(0, 314); + this.statusStrip1.Location = new System.Drawing.Point(0, 362); this.statusStrip1.Name = "statusStrip1"; - this.statusStrip1.Size = new System.Drawing.Size(612, 25); + this.statusStrip1.Size = new System.Drawing.Size(612, 26); this.statusStrip1.TabIndex = 0; this.statusStrip1.Text = "statusStrip1"; // @@ -81,23 +84,22 @@ // this.toolStripStatusLabel1.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.toolStripStatusLabel1.Name = "toolStripStatusLabel1"; - this.toolStripStatusLabel1.Size = new System.Drawing.Size(57, 20); + this.toolStripStatusLabel1.Size = new System.Drawing.Size(64, 21); this.toolStripStatusLabel1.Text = "Status:"; // // toolStripStatusLabel2 // - this.toolStripStatusLabel2.Name = "toolStripStatusLabel2"; - this.toolStripStatusLabel2.Size = new System.Drawing.Size(16, 20); - // Use BackColor only to indicate status. Text is a single space so the label renders as a square. - this.toolStripStatusLabel2.Text = " "; - this.toolStripStatusLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; this.toolStripStatusLabel2.BackColor = System.Drawing.Color.Red; + this.toolStripStatusLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.toolStripStatusLabel2.Name = "toolStripStatusLabel2"; + this.toolStripStatusLabel2.Size = new System.Drawing.Size(12, 21); + this.toolStripStatusLabel2.Text = " "; // // toolStripStatusLabel4 // this.toolStripStatusLabel4.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.toolStripStatusLabel4.Name = "toolStripStatusLabel4"; - this.toolStripStatusLabel4.Size = new System.Drawing.Size(262, 20); + this.toolStripStatusLabel4.Size = new System.Drawing.Size(260, 21); this.toolStripStatusLabel4.Spring = true; this.toolStripStatusLabel4.Text = "Profile:"; this.toolStripStatusLabel4.Click += new System.EventHandler(this.toolStripStatusLabel4_Click); @@ -105,7 +107,7 @@ // toolStripStatusLabel3 // this.toolStripStatusLabel3.Name = "toolStripStatusLabel3"; - this.toolStripStatusLabel3.Size = new System.Drawing.Size(262, 20); + this.toolStripStatusLabel3.Size = new System.Drawing.Size(260, 21); this.toolStripStatusLabel3.Spring = true; this.toolStripStatusLabel3.Text = "Song Changed Updateing"; this.toolStripStatusLabel3.TextAlign = System.Drawing.ContentAlignment.MiddleRight; @@ -117,7 +119,7 @@ this.aboutToolStripMenuItem}); this.menuStrip2.Location = new System.Drawing.Point(0, 0); this.menuStrip2.Name = "menuStrip2"; - this.menuStrip2.Size = new System.Drawing.Size(612, 24); + this.menuStrip2.Size = new System.Drawing.Size(612, 25); this.menuStrip2.TabIndex = 2; this.menuStrip2.Text = "menuStrip2"; // @@ -128,7 +130,7 @@ this.loggingToolStripMenuItem, this.exitToolStripMenuItem}); this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; - this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20); + this.fileToolStripMenuItem.Size = new System.Drawing.Size(41, 21); this.fileToolStripMenuItem.Text = "File"; // // restAPISettingToolStripMenuItem @@ -151,13 +153,13 @@ // onToolStripMenuItem // this.onToolStripMenuItem.Name = "onToolStripMenuItem"; - this.onToolStripMenuItem.Size = new System.Drawing.Size(91, 22); + this.onToolStripMenuItem.Size = new System.Drawing.Size(98, 22); this.onToolStripMenuItem.Text = "On"; // // offToolStripMenuItem // this.offToolStripMenuItem.Name = "offToolStripMenuItem"; - this.offToolStripMenuItem.Size = new System.Drawing.Size(91, 22); + this.offToolStripMenuItem.Size = new System.Drawing.Size(98, 22); this.offToolStripMenuItem.Text = "Off"; // // exitToolStripMenuItem @@ -170,12 +172,15 @@ // aboutToolStripMenuItem // this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem"; - this.aboutToolStripMenuItem.Size = new System.Drawing.Size(52, 20); + this.aboutToolStripMenuItem.Size = new System.Drawing.Size(52, 21); this.aboutToolStripMenuItem.Text = "About"; this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click); // // groupBox1 // + this.groupBox1.Controls.Add(this.time_data_label); + this.groupBox1.Controls.Add(this.label9); + this.groupBox1.Controls.Add(this.time_label); this.groupBox1.Controls.Add(this.label8); this.groupBox1.Controls.Add(this.label7); this.groupBox1.Controls.Add(this.label6); @@ -187,15 +192,34 @@ this.groupBox1.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.groupBox1.Location = new System.Drawing.Point(199, 37); this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(390, 173); + this.groupBox1.Size = new System.Drawing.Size(390, 205); this.groupBox1.TabIndex = 4; this.groupBox1.TabStop = false; this.groupBox1.Text = "Song Info"; // + // label8 + // + this.label8.AutoSize = true; + this.label8.Location = new System.Drawing.Point(20, 103); + this.label8.Name = "label8"; + this.label8.Size = new System.Drawing.Size(357, 16); + this.label8.TabIndex = 7; + this.label8.Text = "――――――――――――――――――――――――――――――――――――――――――――――――――"; + this.label8.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + // + // label7 + // + this.label7.AutoSize = true; + this.label7.Location = new System.Drawing.Point(19, 61); + this.label7.Name = "label7"; + this.label7.Size = new System.Drawing.Size(357, 16); + this.label7.TabIndex = 6; + this.label7.Text = "――――――――――――――――――――――――――――――――――――――――――――――――――"; + // // label6 // this.label6.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label6.Location = new System.Drawing.Point(99, 140); + this.label6.Location = new System.Drawing.Point(99, 121); this.label6.Name = "label6"; this.label6.Size = new System.Drawing.Size(266, 17); this.label6.TabIndex = 5; @@ -205,7 +229,7 @@ // label5 // this.label5.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label5.Location = new System.Drawing.Point(99, 84); + this.label5.Location = new System.Drawing.Point(99, 76); this.label5.Name = "label5"; this.label5.Size = new System.Drawing.Size(266, 17); this.label5.TabIndex = 4; @@ -226,7 +250,7 @@ // this.label3.AutoSize = true; this.label3.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, ((System.Drawing.FontStyle)((System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline))), System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label3.Location = new System.Drawing.Point(23, 138); + this.label3.Location = new System.Drawing.Point(23, 116); this.label3.Name = "label3"; this.label3.Size = new System.Drawing.Size(76, 24); this.label3.TabIndex = 2; @@ -236,7 +260,7 @@ // this.label2.AutoSize = true; this.label2.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, ((System.Drawing.FontStyle)((System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline))), System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label2.Location = new System.Drawing.Point(23, 82); + this.label2.Location = new System.Drawing.Point(23, 75); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(62, 24); this.label2.TabIndex = 1; @@ -254,7 +278,7 @@ // // button1 // - this.button1.Location = new System.Drawing.Point(62, 230); + this.button1.Location = new System.Drawing.Point(62, 281); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(99, 42); this.button1.TabIndex = 5; @@ -264,7 +288,7 @@ // // button2 // - this.button2.Location = new System.Drawing.Point(180, 230); + this.button2.Location = new System.Drawing.Point(180, 281); this.button2.Name = "button2"; this.button2.Size = new System.Drawing.Size(99, 42); this.button2.TabIndex = 6; @@ -275,7 +299,7 @@ // uicolor // this.uicolor.Image = global::RadioDJViewer.Properties.Resources.MdiWeatherNight; - this.uicolor.Location = new System.Drawing.Point(572, 276); + this.uicolor.Location = new System.Drawing.Point(572, 327); this.uicolor.Name = "uicolor"; this.uicolor.Size = new System.Drawing.Size(28, 23); this.uicolor.TabIndex = 7; @@ -283,7 +307,7 @@ // // pictureBox1 // - this.pictureBox1.Location = new System.Drawing.Point(13, 37); + this.pictureBox1.Location = new System.Drawing.Point(13, 69); this.pictureBox1.Name = "pictureBox1"; this.pictureBox1.Size = new System.Drawing.Size(180, 173); this.pictureBox1.TabIndex = 3; @@ -292,7 +316,7 @@ // profile_drop_box // this.profile_drop_box.FormattingEnabled = true; - this.profile_drop_box.Location = new System.Drawing.Point(62, 278); + this.profile_drop_box.Location = new System.Drawing.Point(62, 329); this.profile_drop_box.Name = "profile_drop_box"; this.profile_drop_box.Size = new System.Drawing.Size(217, 21); this.profile_drop_box.TabIndex = 8; @@ -300,7 +324,7 @@ // profile_label // this.profile_label.AutoSize = true; - this.profile_label.Location = new System.Drawing.Point(12, 281); + this.profile_label.Location = new System.Drawing.Point(12, 332); this.profile_label.Name = "profile_label"; this.profile_label.Size = new System.Drawing.Size(44, 13); this.profile_label.TabIndex = 9; @@ -309,37 +333,49 @@ // // load_profile_button // - this.load_profile_button.Location = new System.Drawing.Point(285, 276); + this.load_profile_button.Location = new System.Drawing.Point(285, 327); this.load_profile_button.Name = "load_profile_button"; this.load_profile_button.Size = new System.Drawing.Size(75, 23); this.load_profile_button.TabIndex = 10; this.load_profile_button.Text = "Load Profile"; this.load_profile_button.UseVisualStyleBackColor = true; // - // label7 + // time_label // - this.label7.AutoSize = true; - this.label7.Location = new System.Drawing.Point(19, 61); - this.label7.Name = "label7"; - this.label7.Size = new System.Drawing.Size(357, 16); - this.label7.TabIndex = 6; - this.label7.Text = "――――――――――――――――――――――――――――――――――――――――――――――――――"; + this.time_label.AutoSize = true; + this.time_label.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, ((System.Drawing.FontStyle)((System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline))), System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.time_label.Location = new System.Drawing.Point(20, 157); + this.time_label.Name = "time_label"; + this.time_label.Size = new System.Drawing.Size(63, 24); + this.time_label.TabIndex = 8; + this.time_label.Text = "Time:"; + this.time_label.Click += new System.EventHandler(this.label9_Click); // - // label8 + // label9 // - this.label8.AutoSize = true; - this.label8.Location = new System.Drawing.Point(24, 118); - this.label8.Name = "label8"; - this.label8.Size = new System.Drawing.Size(357, 16); - this.label8.TabIndex = 7; - this.label8.Text = "――――――――――――――――――――――――――――――――――――――――――――――――――"; - this.label8.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + this.label9.AutoSize = true; + this.label9.Location = new System.Drawing.Point(17, 144); + this.label9.Name = "label9"; + this.label9.Size = new System.Drawing.Size(357, 16); + this.label9.TabIndex = 9; + this.label9.Text = "――――――――――――――――――――――――――――――――――――――――――――――――――"; + this.label9.TextAlign = System.Drawing.ContentAlignment.BottomCenter; + // + // time_data_label + // + this.time_data_label.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.time_data_label.Location = new System.Drawing.Point(97, 160); + this.time_data_label.Name = "time_data_label"; + this.time_data_label.Size = new System.Drawing.Size(266, 17); + this.time_data_label.TabIndex = 10; + this.time_data_label.Text = "No Data"; + this.time_data_label.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // // Main // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(612, 339); + this.ClientSize = new System.Drawing.Size(612, 388); this.Controls.Add(this.load_profile_button); this.Controls.Add(this.profile_label); this.Controls.Add(this.profile_drop_box); @@ -396,6 +432,9 @@ private System.Windows.Forms.Button load_profile_button; private System.Windows.Forms.Label label7; private System.Windows.Forms.Label label8; + private System.Windows.Forms.Label time_label; + private System.Windows.Forms.Label label9; + private System.Windows.Forms.Label time_data_label; } } diff --git a/RadioDJViewer/Main.cs b/RadioDJViewer/Main.cs index 50428e3..8143543 100644 --- a/RadioDJViewer/Main.cs +++ b/RadioDJViewer/Main.cs @@ -11,12 +11,18 @@ using System.Xml; using System.Threading; using System.IO; using System.Resources; +using System.Globalization; namespace RadioDJViewer { public partial class Main : Form { private string _currentTrackKey = string.Empty; + private string _playbackTrackKey = string.Empty; + private double _playbackElapsedSeconds = 0; + private double _playbackDurationSeconds = 0; + private bool _playbackPaused = true; + private DateTime _playbackBaseUtc = DateTime.UtcNow; private string selectedFolderPath = string.Empty; private string outputFolderPath = string.Empty; private bool isConnected = false; @@ -31,6 +37,7 @@ namespace RadioDJViewer // Timer to poll REST API private System.Windows.Forms.Timer apiTimer; + private System.Windows.Forms.Timer playbackTimer; private string lastApiXml = null; private System.Windows.Forms.Timer marqueeTimerTitle; @@ -63,6 +70,9 @@ namespace RadioDJViewer apiTimer = new System.Windows.Forms.Timer(); apiTimer.Interval = 3000; // 3 seconds apiTimer.Tick += ApiTimer_Tick; + playbackTimer = new System.Windows.Forms.Timer(); + playbackTimer.Interval = 250; + playbackTimer.Tick += PlaybackTimer_Tick; // Marquee timers for each label marqueeTimerTitle = new System.Windows.Forms.Timer(); marqueeTimerTitle.Interval = marqueeScrollSpeed; @@ -275,22 +285,18 @@ namespace RadioDJViewer if (response.IsSuccessStatusCode) { var xml = await response.Content.ReadAsStringAsync(); - if (xml != lastApiXml) + lastApiXml = xml; + ParseAndDisplaySongInfo(xml); + try { - 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)) { - var newKey = $"{marqueeTextArtist}|{marqueeTextTitle}"; - if (!string.Equals(newKey, _currentTrackKey, StringComparison.Ordinal)) - { - _currentTrackKey = newKey; - ShowTemporaryStatus("Song Change Detected", 2000); - } + _currentTrackKey = newKey; + ShowTemporaryStatus("Song Change Detected", 2000); } - catch { } } + catch { } } } catch { } @@ -332,6 +338,16 @@ namespace RadioDJViewer // Disconnect isConnected = false; apiTimer.Stop(); + playbackTimer.Stop(); + _playbackElapsedSeconds = 0; + _playbackDurationSeconds = 0; + _playbackPaused = true; + _playbackTrackKey = string.Empty; + time_data_label.Text = "No Data"; + WidgetServer.CachedElapsed = "0"; + WidgetServer.CachedDuration = "0"; + WidgetServer.CachedTime = "No Data"; + WidgetServer.CachedPaused = true; // Stop widget server when disconnecting try { @@ -492,6 +508,82 @@ namespace RadioDJViewer marqueeTimerAlbum.Stop(); } + private void PlaybackTimer_Tick(object sender, EventArgs e) + { + UpdatePlaybackDisplay(); + } + + private void UpdatePlaybackState(string trackKey, string elapsedText, string durationText) + { + double elapsedSeconds; + double durationSeconds; + if (!TryParseSeconds(elapsedText, out elapsedSeconds) || !TryParseSeconds(durationText, out durationSeconds) || durationSeconds <= 0) + { + playbackTimer.Stop(); + _playbackTrackKey = trackKey ?? string.Empty; + _playbackElapsedSeconds = 0; + _playbackDurationSeconds = 0; + _playbackPaused = true; + time_data_label.Text = "No Data"; + WidgetServer.CachedElapsed = "0"; + WidgetServer.CachedDuration = "0"; + WidgetServer.CachedTime = "No Data"; + WidgetServer.CachedPaused = true; + return; + } + + bool sameTrack = string.Equals(trackKey, _playbackTrackKey, StringComparison.Ordinal); + bool sameElapsed = Math.Abs(elapsedSeconds - _playbackElapsedSeconds) < 0.01; + bool sameDuration = Math.Abs(durationSeconds - _playbackDurationSeconds) < 0.01; + + _playbackTrackKey = trackKey ?? string.Empty; + _playbackElapsedSeconds = elapsedSeconds; + _playbackDurationSeconds = durationSeconds; + _playbackBaseUtc = DateTime.UtcNow; + _playbackPaused = sameTrack && sameElapsed && sameDuration; + + if (_playbackPaused) + playbackTimer.Stop(); + else + playbackTimer.Start(); + + UpdatePlaybackDisplay(); + } + + private void UpdatePlaybackDisplay() + { + double currentElapsed = GetCurrentElapsedSeconds(); + string timeDisplay = FormatTimeDisplay(currentElapsed, _playbackDurationSeconds); + time_data_label.Text = timeDisplay; + WidgetServer.CachedElapsed = currentElapsed.ToString("0.###", CultureInfo.InvariantCulture); + WidgetServer.CachedDuration = _playbackDurationSeconds.ToString("0.###", CultureInfo.InvariantCulture); + WidgetServer.CachedTime = timeDisplay; + WidgetServer.CachedPaused = _playbackPaused; + } + + private double GetCurrentElapsedSeconds() + { + if (_playbackDurationSeconds <= 0) + return 0; + + double currentElapsed = _playbackElapsedSeconds; + if (!_playbackPaused) + currentElapsed += (DateTime.UtcNow - _playbackBaseUtc).TotalSeconds; + + if (currentElapsed < 0) + currentElapsed = 0; + if (currentElapsed > _playbackDurationSeconds) + currentElapsed = _playbackDurationSeconds; + + return currentElapsed; + } + + private bool TryParseSeconds(string value, out double seconds) + { + return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out seconds) + || double.TryParse(value, NumberStyles.Float, CultureInfo.CurrentCulture, out seconds); + } + private void ParseAndDisplaySongInfo(string xml) { try @@ -503,12 +595,17 @@ namespace RadioDJViewer 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; + // Parse elapsed and duration time (case-insensitive, floating-point seconds) + string elapsed = doc.SelectSingleNode("//*[translate(local-name(), 'ELAPSED', 'elapsed')='elapsed']")?.InnerText; + string duration = doc.SelectSingleNode("//*[translate(local-name(), 'DURATION', 'duration')='duration']")?.InnerText; marqueeTextTitle = !string.IsNullOrWhiteSpace(title) ? title : "No title"; marqueeTextArtist = !string.IsNullOrWhiteSpace(artist) ? artist : "No artist"; marqueeTextAlbum = !string.IsNullOrWhiteSpace(album) ? album : "No album"; StartMarqueeTimers(); + UpdatePlaybackState($"{marqueeTextArtist}|{marqueeTextTitle}", elapsed, duration); + // Write to output files if (!string.IsNullOrEmpty(outputFolderPath)) { @@ -522,6 +619,7 @@ namespace RadioDJViewer { WidgetServer.CachedArtist = marqueeTextArtist ?? string.Empty; WidgetServer.CachedTitle = marqueeTextTitle ?? string.Empty; + byte[] imgBytes = null; // Prefer album art from main images folder if (!string.IsNullOrEmpty(albumArt) && !string.IsNullOrEmpty(mainImagesFolderPath)) @@ -576,32 +674,38 @@ namespace RadioDJViewer marqueeTextArtist = "No artist"; marqueeTextAlbum = "No album"; StartMarqueeTimers(); + playbackTimer.Stop(); + _playbackElapsedSeconds = 0; + _playbackDurationSeconds = 0; + _playbackPaused = true; + time_data_label.Text = "No Data"; + WidgetServer.CachedElapsed = "0"; + WidgetServer.CachedDuration = "0"; + WidgetServer.CachedTime = "No Data"; + WidgetServer.CachedPaused = true; pictureBox1.Image = null; } } private void UpdateStatusBar() { - // Update color box based on connection states try { if (!isConnected) { - toolStripStatusLabel2.BackColor = Color.Red; // disconnected/error + toolStripStatusLabel2.BackColor = Color.Red; } else { if (isConnected && widgetRunning) - toolStripStatusLabel2.BackColor = Color.Blue; // both running + toolStripStatusLabel2.BackColor = Color.Blue; else - toolStripStatusLabel2.BackColor = Color.Green; // API ok, web off + toolStripStatusLabel2.BackColor = Color.Green; } } 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"; @@ -610,7 +714,6 @@ namespace RadioDJViewer private void ShowTemporaryStatus(string message, int milliseconds) { - // Cancel any previous temporary status try { statusCts?.Cancel(); @@ -620,7 +723,6 @@ namespace RadioDJViewer 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; @@ -638,7 +740,6 @@ namespace RadioDJViewer await Task.Delay(milliseconds, cts.Token); if (!cts.IsCancellationRequested) { - // clear temporary message statusCts = null; this.BeginInvoke((Action)(() => { toolStripStatusLabel3.Text = isConnected ? "Connected to API" : "Disconnected"; @@ -658,12 +759,13 @@ namespace RadioDJViewer private void StartWidgetServerIfEnabled() { - // Called after API connection is confirmed if (loadedProfile == null || !loadedProfile.WebServerEnabled) return; try { + WidgetServer.CachedClockVisibility = loadedProfile.ClockVisibilityEnabled; + if (widgetServer == null) { int port = loadedProfile.WebServerPort > 0 ? loadedProfile.WebServerPort : 8080; @@ -678,16 +780,14 @@ namespace RadioDJViewer { 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.ApplyTheme(isDarkMode); settingsForm.ShowDialog(); } @@ -708,32 +808,28 @@ namespace RadioDJViewer 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 + mainImagesFolderPath = profile.MainImagesFolder; + loadedProfile = profile; 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; + WidgetServer.CachedClockVisibility = profile.ClockVisibilityEnabled; UpdateStatusBar(); - // You can add more logic to update other fields if needed } private void Uicolor_Click(object sender, EventArgs e) @@ -759,23 +855,19 @@ namespace RadioDJViewer 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 + var resizedIcon = new Bitmap(icon, btnW - 8, btnH - 8); 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; @@ -798,7 +890,10 @@ namespace RadioDJViewer private void label7_Click(object sender, EventArgs e) { + } + private void label9_Click(object sender, EventArgs e) + { } public string GetCurrentProfileName() @@ -811,7 +906,6 @@ namespace RadioDJViewer 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; @@ -832,5 +926,49 @@ namespace RadioDJViewer } } } + + private string FormatTimeDisplay(string elapsedSeconds, string durationSeconds) + { + double elapsed; + double duration; + if (!TryParseSeconds(elapsedSeconds, out elapsed) || !TryParseSeconds(durationSeconds, out duration)) + { + return "No Data"; + } + + return FormatTimeDisplay(elapsed, duration); + } + + private string FormatTimeDisplay(double elapsed, double duration) + { + // If duration is zero or negative, show "No Data" + if (duration <= 0) + { + return "No Data"; + } + + // Clamp elapsed to [0, duration] + if (elapsed < 0) + elapsed = 0; + if (elapsed > duration) + elapsed = duration; + + // Convert to integers (seconds only) + int elapsedInt = (int)elapsed; + int durationInt = (int)duration; + + // Convert seconds to MM:SS format + string elapsedFormatted = FormatSeconds(elapsedInt); + string durationFormatted = FormatSeconds(durationInt); + + return $"{elapsedFormatted} / {durationFormatted}"; + } + + private string FormatSeconds(int seconds) + { + int minutes = seconds / 60; + int secs = seconds % 60; + return $"{minutes}:{secs:D2}"; + } } } diff --git a/RadioDJViewer/Main.resx b/RadioDJViewer/Main.resx index 1cf2200..9cb2abd 100644 --- a/RadioDJViewer/Main.resx +++ b/RadioDJViewer/Main.resx @@ -120,17 +120,10 @@ 17, 17 - - - - iVBORw0KGgoAAAANSUhEUgAAAIAAAACABAMAAAAxEHz4AAAABGdBTUEAALGPC/xhBQAAAANQTFRFJn8A - Mx1isQAAAAlwSFlzAAAOvAAADrwBlbxySQAAAB9JREFUaN7twTEBAAAAwqD1T20IX6AAAAAAAAAAAD4D - IIAAAT2Y3yoAAAAASUVORK5CYII= - - 247, 17 + AAABAAEAIBgAAAEAIACIDAAAFgAAACgAAAAgAAAAMAAAAAEAIAAAAAAAAAwAAMMOAADDDgAAAAAAAAAA diff --git a/RadioDJViewer/ProfileStorage.cs b/RadioDJViewer/ProfileStorage.cs index 34797f6..6759bf1 100644 --- a/RadioDJViewer/ProfileStorage.cs +++ b/RadioDJViewer/ProfileStorage.cs @@ -23,6 +23,8 @@ namespace RadioDJViewer // Web server settings public bool WebServerEnabled { get; set; } public int WebServerPort { get; set; } + // Clock visibility setting + public bool ClockVisibilityEnabled { get; set; } } public static class ProfileStorage diff --git a/RadioDJViewer/Properties/AssemblyInfo.cs b/RadioDJViewer/Properties/AssemblyInfo.cs index ef23503..35b4986 100644 --- a/RadioDJViewer/Properties/AssemblyInfo.cs +++ b/RadioDJViewer/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("1.9.0")] -[assembly: AssemblyFileVersion("1.9.0")] +[assembly: AssemblyVersion("1.10.0")] +[assembly: AssemblyFileVersion("1.10.0")] diff --git a/RadioDJViewer/WidgetServer.cs b/RadioDJViewer/WidgetServer.cs index 7667b57..84a4765 100644 --- a/RadioDJViewer/WidgetServer.cs +++ b/RadioDJViewer/WidgetServer.cs @@ -21,7 +21,12 @@ namespace RadioDJViewer // In-memory cache for fast serving public static string CachedArtist = ""; public static string CachedTitle = ""; + public static string CachedTime = "No Data"; + public static string CachedElapsed = "0"; + public static string CachedDuration = "0"; + public static bool CachedPaused = true; public static byte[] CachedImageBytes = null; + public static bool CachedClockVisibility = false; private readonly int port; private readonly Func getArtist; @@ -112,14 +117,35 @@ namespace RadioDJViewer var html = htmlTemplate ?? ""; // Replace placeholders with in-memory cache values html = html.Replace("{{ARTIST}}", WebUtility.HtmlEncode(CachedArtist ?? string.Empty)) - .Replace("{{TITLE}}", WebUtility.HtmlEncode(CachedTitle ?? string.Empty)); + .Replace("{{TITLE}}", WebUtility.HtmlEncode(CachedTitle ?? string.Empty)) + .Replace("{{TIME}}", WebUtility.HtmlEncode(CachedTime ?? string.Empty)); var buf = Encoding.UTF8.GetBytes(html); resp.ContentType = "text/html; charset=utf-8"; resp.ContentLength64 = buf.Length; resp.OutputStream.Write(buf, 0, buf.Length); } + else if (path.Equals("/status.json", StringComparison.OrdinalIgnoreCase)) + { + var json = "{" + + $"\"artist\":\"{JsonEscape(CachedArtist ?? string.Empty)}\"," + + $"\"title\":\"{JsonEscape(CachedTitle ?? string.Empty)}\"," + + $"\"time\":\"{JsonEscape(CachedTime ?? "No Data")}\"," + + $"\"elapsed\":\"{JsonEscape(CachedElapsed ?? "0")}\"," + + $"\"duration\":\"{JsonEscape(CachedDuration ?? "0")}\"," + + $"\"paused\":{(CachedPaused ? "true" : "false")}," + + $"\"clockVisible\":{(CachedClockVisibility ? "true" : "false")}" + + "}"; + var buf = Encoding.UTF8.GetBytes(json); + resp.ContentType = "application/json; charset=utf-8"; + resp.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"; + resp.Headers["Pragma"] = "no-cache"; + resp.Headers["Expires"] = "0"; + resp.ContentLength64 = buf.Length; + resp.OutputStream.Write(buf, 0, buf.Length); + } else if (path.Equals("/artist.txt", StringComparison.OrdinalIgnoreCase)) { + ApplyNoCache(resp); var buf = Encoding.UTF8.GetBytes(CachedArtist ?? string.Empty); resp.ContentType = "text/plain; charset=utf-8"; resp.ContentLength64 = buf.Length; @@ -127,11 +153,44 @@ namespace RadioDJViewer } else if (path.Equals("/title.txt", StringComparison.OrdinalIgnoreCase)) { + ApplyNoCache(resp); var buf = Encoding.UTF8.GetBytes(CachedTitle ?? string.Empty); resp.ContentType = "text/plain; charset=utf-8"; resp.ContentLength64 = buf.Length; resp.OutputStream.Write(buf, 0, buf.Length); } + else if (path.Equals("/time.txt", StringComparison.OrdinalIgnoreCase)) + { + ApplyNoCache(resp); + var buf = Encoding.UTF8.GetBytes(CachedTime ?? "No Data"); + resp.ContentType = "text/plain; charset=utf-8"; + resp.ContentLength64 = buf.Length; + resp.OutputStream.Write(buf, 0, buf.Length); + } + else if (path.Equals("/elapsed", StringComparison.OrdinalIgnoreCase)) + { + ApplyNoCache(resp); + var buf = Encoding.UTF8.GetBytes(CachedElapsed ?? "0"); + resp.ContentType = "text/plain; charset=utf-8"; + resp.ContentLength64 = buf.Length; + resp.OutputStream.Write(buf, 0, buf.Length); + } + else if (path.Equals("/duration", StringComparison.OrdinalIgnoreCase)) + { + ApplyNoCache(resp); + var buf = Encoding.UTF8.GetBytes(CachedDuration ?? "0"); + resp.ContentType = "text/plain; charset=utf-8"; + resp.ContentLength64 = buf.Length; + resp.OutputStream.Write(buf, 0, buf.Length); + } + else if (path.Equals("/clock-visibility", StringComparison.OrdinalIgnoreCase)) + { + ApplyNoCache(resp); + var buf = Encoding.UTF8.GetBytes(CachedClockVisibility ? "true" : "false"); + resp.ContentType = "text/plain; charset=utf-8"; + resp.ContentLength64 = buf.Length; + resp.OutputStream.Write(buf, 0, buf.Length); + } else if (path.Equals("/style.css", StringComparison.OrdinalIgnoreCase)) { var buf = Encoding.UTF8.GetBytes(cssText ?? ""); @@ -209,6 +268,24 @@ namespace RadioDJViewer } } + private static void ApplyNoCache(HttpListenerResponse resp) + { + resp.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"; + resp.Headers["Pragma"] = "no-cache"; + resp.Headers["Expires"] = "0"; + } + + private static string JsonEscape(string value) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + return value.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n"); + } + private string GetMimeTypeFromPath(string path) { var ext = Path.GetExtension(path).ToLowerInvariant(); diff --git a/RadioDJViewer/index.html b/RadioDJViewer/index.html index 8e7abb8..a5d50da 100644 --- a/RadioDJViewer/index.html +++ b/RadioDJViewer/index.html @@ -25,8 +25,9 @@
-
Loading...
-
Loading...
+
{{TITLE}}
+
{{ARTIST}}
+
{{TIME}}
diff --git a/RadioDJViewer/radiodj-restapi-template.Designer.cs b/RadioDJViewer/radiodj-restapi-template.Designer.cs index 83195f2..18e98e0 100644 --- a/RadioDJViewer/radiodj-restapi-template.Designer.cs +++ b/RadioDJViewer/radiodj-restapi-template.Designer.cs @@ -56,6 +56,11 @@ this.label7 = new System.Windows.Forms.Label(); this.textBox7 = new System.Windows.Forms.TextBox(); this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.web_server_settings_groupbox = new System.Windows.Forms.GroupBox(); + this.web_server_port_textbox = new System.Windows.Forms.TextBox(); + this.web_server_port_label = new System.Windows.Forms.Label(); + this.web_server_label = new System.Windows.Forms.Label(); + this.web_server_checkbox = new System.Windows.Forms.CheckBox(); this.label13 = new System.Windows.Forms.Label(); this.comboBoxScrollSpeed = new System.Windows.Forms.ComboBox(); this.textBoxPauseTime = new System.Windows.Forms.TextBox(); @@ -64,11 +69,8 @@ this.label9 = new System.Windows.Forms.Label(); this.label8 = new System.Windows.Forms.Label(); this.label12 = new System.Windows.Forms.Label(); - this.web_server_label = new System.Windows.Forms.Label(); - this.web_server_checkbox = new System.Windows.Forms.CheckBox(); - this.web_server_settings_groupbox = new System.Windows.Forms.GroupBox(); - this.web_server_port_label = new System.Windows.Forms.Label(); - this.web_server_port_textbox = new System.Windows.Forms.TextBox(); + this.clock_label = new System.Windows.Forms.Label(); + this.clock_visibility_checkBox = new System.Windows.Forms.CheckBox(); this.groupBox1.SuspendLayout(); this.groupBox3.SuspendLayout(); this.groupBox2.SuspendLayout(); @@ -104,7 +106,7 @@ // // ok_button_master // - this.ok_button_master.Location = new System.Drawing.Point(65, 648); + this.ok_button_master.Location = new System.Drawing.Point(65, 694); this.ok_button_master.Name = "ok_button_master"; this.ok_button_master.Size = new System.Drawing.Size(157, 23); this.ok_button_master.TabIndex = 1; @@ -113,7 +115,7 @@ // // button2 // - this.button2.Location = new System.Drawing.Point(305, 648); + this.button2.Location = new System.Drawing.Point(305, 694); this.button2.Name = "button2"; this.button2.Size = new System.Drawing.Size(157, 23); this.button2.TabIndex = 2; @@ -187,7 +189,7 @@ // // button3 // - this.button3.Location = new System.Drawing.Point(71, 491); + this.button3.Location = new System.Drawing.Point(71, 535); this.button3.Name = "button3"; this.button3.Size = new System.Drawing.Size(120, 30); this.button3.TabIndex = 8; @@ -196,7 +198,7 @@ // // button4 // - this.button4.Location = new System.Drawing.Point(316, 491); + this.button4.Location = new System.Drawing.Point(316, 535); this.button4.Name = "button4"; this.button4.Size = new System.Drawing.Size(120, 30); this.button4.TabIndex = 9; @@ -209,7 +211,7 @@ this.groupBox3.Controls.Add(this.labelOutputFolderPath); this.groupBox3.Controls.Add(this.textBox5); this.groupBox3.Controls.Add(this.label10); - this.groupBox3.Location = new System.Drawing.Point(31, 370); + this.groupBox3.Location = new System.Drawing.Point(22, 413); this.groupBox3.Name = "groupBox3"; this.groupBox3.Size = new System.Drawing.Size(482, 115); this.groupBox3.TabIndex = 12; @@ -329,11 +331,61 @@ this.groupBox2.Controls.Add(this.comboBox1); this.groupBox2.Location = new System.Drawing.Point(12, 100); this.groupBox2.Name = "groupBox2"; - this.groupBox2.Size = new System.Drawing.Size(519, 527); + this.groupBox2.Size = new System.Drawing.Size(519, 575); this.groupBox2.TabIndex = 3; this.groupBox2.TabStop = false; this.groupBox2.Text = "Profiles"; // + // web_server_settings_groupbox + // + this.web_server_settings_groupbox.Controls.Add(this.clock_visibility_checkBox); + this.web_server_settings_groupbox.Controls.Add(this.clock_label); + this.web_server_settings_groupbox.Controls.Add(this.web_server_port_textbox); + this.web_server_settings_groupbox.Controls.Add(this.web_server_port_label); + this.web_server_settings_groupbox.Controls.Add(this.web_server_label); + this.web_server_settings_groupbox.Controls.Add(this.web_server_checkbox); + this.web_server_settings_groupbox.Location = new System.Drawing.Point(31, 308); + this.web_server_settings_groupbox.Name = "web_server_settings_groupbox"; + this.web_server_settings_groupbox.Size = new System.Drawing.Size(471, 99); + this.web_server_settings_groupbox.TabIndex = 35; + this.web_server_settings_groupbox.TabStop = false; + this.web_server_settings_groupbox.Text = "Web Server Settings"; + // + // web_server_port_textbox + // + this.web_server_port_textbox.Location = new System.Drawing.Point(246, 19); + this.web_server_port_textbox.Name = "web_server_port_textbox"; + this.web_server_port_textbox.Size = new System.Drawing.Size(77, 20); + this.web_server_port_textbox.TabIndex = 36; + // + // web_server_port_label + // + this.web_server_port_label.AutoSize = true; + this.web_server_port_label.Location = new System.Drawing.Point(211, 23); + this.web_server_port_label.Name = "web_server_port_label"; + this.web_server_port_label.Size = new System.Drawing.Size(29, 13); + this.web_server_port_label.TabIndex = 35; + this.web_server_port_label.Text = "Port:"; + // + // web_server_label + // + this.web_server_label.AutoSize = true; + this.web_server_label.Location = new System.Drawing.Point(44, 24); + this.web_server_label.Name = "web_server_label"; + this.web_server_label.Size = new System.Drawing.Size(67, 13); + this.web_server_label.TabIndex = 33; + this.web_server_label.Text = "Web Server:"; + // + // web_server_checkbox + // + this.web_server_checkbox.AutoSize = true; + this.web_server_checkbox.Location = new System.Drawing.Point(111, 23); + this.web_server_checkbox.Name = "web_server_checkbox"; + this.web_server_checkbox.Size = new System.Drawing.Size(65, 17); + this.web_server_checkbox.TabIndex = 34; + this.web_server_checkbox.Text = "Enabled"; + this.web_server_checkbox.UseVisualStyleBackColor = true; + // // label13 // this.label13.AutoSize = true; @@ -407,59 +459,30 @@ this.label12.TabIndex = 27; this.label12.Text = "Separating character format"; // - // web_server_label + // clock_label // - this.web_server_label.AutoSize = true; - this.web_server_label.Location = new System.Drawing.Point(44, 24); - this.web_server_label.Name = "web_server_label"; - this.web_server_label.Size = new System.Drawing.Size(67, 13); - this.web_server_label.TabIndex = 33; - this.web_server_label.Text = "Web Server:"; + this.clock_label.AutoSize = true; + this.clock_label.Location = new System.Drawing.Point(44, 56); + this.clock_label.Name = "clock_label"; + this.clock_label.Size = new System.Drawing.Size(76, 13); + this.clock_label.TabIndex = 37; + this.clock_label.Text = "Clock Visibility:"; // - // web_server_checkbox + // clock_visibility_checkBox // - this.web_server_checkbox.AutoSize = true; - this.web_server_checkbox.Location = new System.Drawing.Point(111, 23); - this.web_server_checkbox.Name = "web_server_checkbox"; - this.web_server_checkbox.Size = new System.Drawing.Size(65, 17); - this.web_server_checkbox.TabIndex = 34; - this.web_server_checkbox.Text = "Enabled"; - this.web_server_checkbox.UseVisualStyleBackColor = true; - // - // web_server_settings_groupbox - // - this.web_server_settings_groupbox.Controls.Add(this.web_server_port_textbox); - this.web_server_settings_groupbox.Controls.Add(this.web_server_port_label); - this.web_server_settings_groupbox.Controls.Add(this.web_server_label); - this.web_server_settings_groupbox.Controls.Add(this.web_server_checkbox); - this.web_server_settings_groupbox.Location = new System.Drawing.Point(31, 298); - this.web_server_settings_groupbox.Name = "web_server_settings_groupbox"; - this.web_server_settings_groupbox.Size = new System.Drawing.Size(396, 58); - this.web_server_settings_groupbox.TabIndex = 35; - this.web_server_settings_groupbox.TabStop = false; - this.web_server_settings_groupbox.Text = "Web Server Settings"; - // - // web_server_port_label - // - this.web_server_port_label.AutoSize = true; - this.web_server_port_label.Location = new System.Drawing.Point(211, 23); - this.web_server_port_label.Name = "web_server_port_label"; - this.web_server_port_label.Size = new System.Drawing.Size(29, 13); - this.web_server_port_label.TabIndex = 35; - this.web_server_port_label.Text = "Port:"; - // - // web_server_port_textbox - // - this.web_server_port_textbox.Location = new System.Drawing.Point(246, 19); - this.web_server_port_textbox.Name = "web_server_port_textbox"; - this.web_server_port_textbox.Size = new System.Drawing.Size(77, 20); - this.web_server_port_textbox.TabIndex = 36; + this.clock_visibility_checkBox.AutoSize = true; + this.clock_visibility_checkBox.Location = new System.Drawing.Point(119, 55); + this.clock_visibility_checkBox.Name = "clock_visibility_checkBox"; + this.clock_visibility_checkBox.Size = new System.Drawing.Size(65, 17); + this.clock_visibility_checkBox.TabIndex = 38; + this.clock_visibility_checkBox.Text = "Enabled"; + this.clock_visibility_checkBox.UseVisualStyleBackColor = true; // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(572, 695); + this.ClientSize = new System.Drawing.Size(572, 729); this.Controls.Add(this.groupBox2); this.Controls.Add(this.button2); this.Controls.Add(this.ok_button_master); @@ -523,5 +546,7 @@ private System.Windows.Forms.CheckBox web_server_checkbox; private System.Windows.Forms.TextBox web_server_port_textbox; private System.Windows.Forms.Label web_server_port_label; + private System.Windows.Forms.CheckBox clock_visibility_checkBox; + private System.Windows.Forms.Label clock_label; } } \ No newline at end of file diff --git a/RadioDJViewer/radiodj-restapi-template.cs b/RadioDJViewer/radiodj-restapi-template.cs index 1dd75d9..f9e4e2e 100644 --- a/RadioDJViewer/radiodj-restapi-template.cs +++ b/RadioDJViewer/radiodj-restapi-template.cs @@ -83,6 +83,8 @@ namespace RadioDJViewer { web_server_checkbox.Checked = profile.WebServerEnabled; web_server_port_textbox.Text = (profile.WebServerPort > 0 ? profile.WebServerPort : 8080).ToString(); + // Clock visibility setting + clock_visibility_checkBox.Checked = profile.ClockVisibilityEnabled; } catch { } } @@ -139,7 +141,9 @@ namespace RadioDJViewer MarqueeSeparator = separatingcharacterformatbox.Text, // Web server settings WebServerEnabled = web_server_checkbox.Checked, - WebServerPort = int.TryParse(web_server_port_textbox.Text, out int wp) ? wp : 8080 + WebServerPort = int.TryParse(web_server_port_textbox.Text, out int wp) ? wp : 8080, + // Clock visibility setting + ClockVisibilityEnabled = clock_visibility_checkBox.Checked }; ProfileStorage.SaveProfile(profile); // Refresh profile list diff --git a/RadioDJViewer/script.js b/RadioDJViewer/script.js index cb437ae..e7fe24a 100644 --- a/RadioDJViewer/script.js +++ b/RadioDJViewer/script.js @@ -1,128 +1,171 @@ -/////////////// -// PARAMETERS // -/////////////// +(function () { + var IMAGE_FILENAME = "Album-Art.png"; + var DEFAULT_REFRESH_RATE_MS = 1000; + var DEFAULT_TIME_REFRESH_RATE_MS = 100; + var currentArtist = getTextContent(".song-artist"); + var currentTitle = getTextContent(".song-title"); + var currentTime = getTextContent(".song-time"); + var refreshInterval = null; + var timeRefreshInterval = null; + var latestTimeRequestId = 0; + var refreshRateMs = getRefreshRateMs(); + var timeRefreshRateMs = DEFAULT_TIME_REFRESH_RATE_MS; -const queryString = window.location.search; -const urlParams = new URLSearchParams(queryString); -const refreshRate = parseInt(urlParams.get("refreshrate")) || 6; // Default 6 seconds - -// Image filename - update this if the backend changes the image path -const IMAGE_FILENAME = "Album-Art.png"; - -////////////////// -// GLOBAL STATE // -////////////////// - -let currentArtist = ""; -let currentTitle = ""; -let refreshInterval = null; - -////////////////// -// REFRESH LOGIC // -////////////////// - -/** - * Fetches the artist and title from text files - */ -async function fetchSongData() { - try { - const artistResponse = await fetch("artist.txt"); - const titleResponse = await fetch("title.txt"); - - if (!artistResponse.ok || !titleResponse.ok) { - console.error("Failed to fetch song data"); - return null; + function getTextContent(selector) { + var element = document.querySelector(selector); + if (!element || typeof element.textContent !== "string") { + return ""; } - - const artist = (await artistResponse.text()).trim(); - const title = (await titleResponse.text()).trim(); - - return { artist, title }; - } catch (error) { - console.error("Error fetching song data:", error); - return null; - } -} - -/** - * Updates the DOM elements if the song data has changed - */ -function updateSongDisplay(artist, title) { - const titleElement = document.querySelector(".song-title"); - const artistElement = document.querySelector(".song-artist"); - - let trackChanged = false; - - // Only update if data has actually changed - if (title !== currentTitle) { - currentTitle = title; - titleElement.classList.add("updating"); - titleElement.textContent = title || "Unknown Title"; - trackChanged = true; - setTimeout(() => { - titleElement.classList.remove("updating"); - }, 400); + return trimText(element.textContent); } - if (artist !== currentArtist) { - currentArtist = artist; - artistElement.classList.add("updating"); - artistElement.textContent = artist || "Unknown Artist"; - trackChanged = true; - setTimeout(() => { - artistElement.classList.remove("updating"); - }, 400); + function trimText(value) { + return String(value || "").replace(/^\s+|\s+$/g, ""); } - // Return whether track changed (to trigger image update) - return trackChanged; -} + function getRefreshRateMs() { + var match = /(?:\?|&)refreshrate=([^&]+)/i.exec(window.location.search || ""); + var seconds = match ? parseFloat(decodeURIComponent(match[1])) : 1; + if (!isFinite(seconds) || seconds <= 0) { + seconds = 1; + } + var interval = Math.round(seconds * 1000); + if (interval < 250) { + interval = 250; + } + return interval; + } -/** - * Updates both the album art and background image with cache-busting timestamp - * Only called when track actually changes - */ -function updateAlbumArt() { - const albumArt = document.querySelector(".album-art"); - const backgroundImage = document.querySelector(".background-image"); - const timestamp = new Date().getTime(); - const imageUrl = `${IMAGE_FILENAME}?v=${timestamp}`; - - albumArt.src = imageUrl; - backgroundImage.src = imageUrl; -} - -/** - * Main refresh function - fetches data and updates if changed - */ -async function refresh() { - const data = await fetchSongData(); - if (data) { - const trackChanged = updateSongDisplay(data.artist, data.title); - // Only update images when track actually changes - if (trackChanged) { - updateAlbumArt(); + function fetchText(path, fallbackValue, callback) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", path + "?v=" + new Date().getTime(), true); + xhr.setRequestHeader("Cache-Control", "no-cache"); + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) { + return; + } + if (xhr.status >= 200 && xhr.status < 300) { + callback(trimText(xhr.responseText)); + return; + } + callback(fallbackValue); + }; + xhr.onerror = function () { + callback(fallbackValue); + }; + try { + xhr.send(null); + } catch (e) { + callback(fallbackValue); } } -} -/////////////// -// INITIALIZE // -/////////////// + function updateSongDisplay(data) { + var titleElement = document.querySelector(".song-title"); + var artistElement = document.querySelector(".song-artist"); + var trackChanged = false; + var nextTitle; + var nextArtist; -function init() { - // Load initial data immediately - refresh(); + if (!titleElement || !artistElement || !data) { + return false; + } - // Set up refresh interval based on URL parameter or default - refreshInterval = setInterval(refresh, refreshRate * 1000); + nextTitle = trimText(data.title || ""); + nextArtist = trimText(data.artist || ""); - console.log(`Radio DJ widget initialized - refreshing every ${refreshRate}s`); -} + if (nextTitle !== currentTitle) { + currentTitle = nextTitle; + titleElement.classList.add("updating"); + titleElement.textContent = currentTitle || "Unknown Title"; + trackChanged = true; + setTimeout(function () { + titleElement.classList.remove("updating"); + }, 400); + } -// Start when DOM is ready -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init); -} else { - init(); -} + if (nextArtist !== currentArtist) { + currentArtist = nextArtist; + artistElement.classList.add("updating"); + artistElement.textContent = currentArtist || "Unknown Artist"; + trackChanged = true; + setTimeout(function () { + artistElement.classList.remove("updating"); + }, 400); + } + + return trackChanged; + } + + function updateTimeDisplay(nextTime) { + var timeElement = document.querySelector(".song-time"); + var normalizedTime = trimText(nextTime || "No Data") || "No Data"; + if (!timeElement) { + return; + } + if (normalizedTime !== currentTime) { + currentTime = normalizedTime; + timeElement.textContent = currentTime; + } + } + + function updateAlbumArt() { + var albumArt = document.querySelector(".album-art"); + var backgroundImage = document.querySelector(".background-image"); + var imageUrl = IMAGE_FILENAME + "?v=" + new Date().getTime(); + if (!albumArt || !backgroundImage) { + return; + } + albumArt.src = imageUrl; + backgroundImage.src = imageUrl; + } + + function setClockVisibility(visible) { + var timeElement = document.querySelector(".song-time"); + if (!timeElement) { + return; + } + timeElement.style.display = visible ? "" : "none"; + } + + function refreshMetadata() { + fetchText("artist.txt", currentArtist || "", function (artist) { + fetchText("title.txt", currentTitle || "", function (title) { + fetchText("clock-visibility", "true", function (clockVisible) { + var trackChanged = updateSongDisplay({ + artist: artist, + title: title + }); + setClockVisibility(String(clockVisible || "false").toLowerCase() === "true"); + if (trackChanged) { + updateAlbumArt(); + } + }); + }); + }); + } + + function refreshTime() { + var requestId = latestTimeRequestId + 1; + latestTimeRequestId = requestId; + fetchText("time.txt", currentTime || "No Data", function (nextTime) { + if (requestId !== latestTimeRequestId) { + return; + } + updateTimeDisplay(nextTime); + }); + } + + function init() { + refreshMetadata(); + refreshTime(); + refreshInterval = window.setInterval(refreshMetadata, refreshRateMs); + timeRefreshInterval = window.setInterval(refreshTime, timeRefreshRateMs); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/RadioDJViewer/style.css b/RadioDJViewer/style.css index ecfa75f..e72cd3d 100644 --- a/RadioDJViewer/style.css +++ b/RadioDJViewer/style.css @@ -127,6 +127,21 @@ body { opacity: 1; } +/* Song time display (underneath artist) */ +.song-time { + font-size: 14px; + font-weight: 400; + color: #cccccc; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.9); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: 0px; + line-height: 1.1; + opacity: 0.9; + margin-top: 4px; +} + /* Smooth fade animation for updates */ @keyframes fadeIn { from { @@ -141,3 +156,22 @@ body { .song-artist.updating { animation: fadeIn 0.4s ease-in-out; } + +/* Clock section - positioned at bottom of page */ +.clock-section { + width: 100%; + margin-top: 20px; + padding: 20px; + display: flex; + justify-content: center; + align-items: center; +} + +.digital-clock { + font-size: 48px; + font-weight: 300; + color: #ffffff; + text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.9); + font-family: 'Courier New', monospace; + letter-spacing: 2px; +}