diff --git a/RadioDJViewer/EmbeddedResourceHelper.cs b/RadioDJViewer/EmbeddedResourceHelper.cs
new file mode 100644
index 0000000..7993398
--- /dev/null
+++ b/RadioDJViewer/EmbeddedResourceHelper.cs
@@ -0,0 +1,41 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace RadioDJViewer
+{
+ public static class EmbeddedResourceHelper
+ {
+ ///
+ /// Reads an embedded resource's text content by matching the resource name ending with the provided file name.
+ /// Example: call Read("index.html") to load a resource named "RadioDJViewer.Web.index.html" or similar.
+ ///
+ public static string Read(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ throw new ArgumentNullException(nameof(fileName));
+
+ var asm = Assembly.GetExecutingAssembly();
+ var resourceName = asm.GetManifestResourceNames()
+ .FirstOrDefault(n => n.EndsWith(fileName, StringComparison.OrdinalIgnoreCase));
+
+ if (resourceName == null)
+ {
+ var names = string.Join(", ", asm.GetManifestResourceNames());
+ throw new FileNotFoundException($"Embedded resource '{fileName}' not found. Available resources: {names}");
+ }
+
+ using (var stream = asm.GetManifestResourceStream(resourceName))
+ {
+ if (stream == null)
+ throw new FileNotFoundException($"Resource stream '{resourceName}' is null.");
+
+ using (var reader = new StreamReader(stream))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+ }
+}
diff --git a/RadioDJViewer/Main.cs b/RadioDJViewer/Main.cs
index 99f78ea..068dfdf 100644
--- a/RadioDJViewer/Main.cs
+++ b/RadioDJViewer/Main.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml;
+using System.Threading;
using System.IO;
using System.Resources;
@@ -15,6 +16,7 @@ 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;
@@ -23,6 +25,9 @@ namespace RadioDJViewer
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;
@@ -38,9 +43,9 @@ namespace RadioDJViewer
private string marqueeTextArtist = "";
private string marqueeTextAlbum = "";
- private Timer pauseTimerTitle;
- private Timer pauseTimerArtist;
- private Timer pauseTimerAlbum;
+ 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
@@ -161,7 +166,7 @@ namespace RadioDJViewer
marqueeTimerTitle.Stop();
if (pauseTimerTitle == null)
{
- pauseTimerTitle = new Timer();
+ pauseTimerTitle = new System.Windows.Forms.Timer();
pauseTimerTitle.Tick += (s, args) => {
pauseTimerTitle.Stop();
marqueeTimerTitle.Start();
@@ -193,7 +198,7 @@ namespace RadioDJViewer
marqueeTimerArtist.Stop();
if (pauseTimerArtist == null)
{
- pauseTimerArtist = new Timer();
+ pauseTimerArtist = new System.Windows.Forms.Timer();
pauseTimerArtist.Tick += (s, args) => {
pauseTimerArtist.Stop();
marqueeTimerArtist.Start();
@@ -225,7 +230,7 @@ namespace RadioDJViewer
marqueeTimerAlbum.Stop();
if (pauseTimerAlbum == null)
{
- pauseTimerAlbum = new Timer();
+ pauseTimerAlbum = new System.Windows.Forms.Timer();
pauseTimerAlbum.Tick += (s, args) => {
pauseTimerAlbum.Stop();
marqueeTimerAlbum.Start();
@@ -274,6 +279,17 @@ namespace RadioDJViewer
{
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 { }
}
}
}
@@ -300,7 +316,10 @@ namespace RadioDJViewer
private async void button1_Click(object sender, EventArgs e)
{
- // Connect to REST API
+ // Show connecting message
+ ShowTemporaryStatus("Connecting to API...", 0);
+
+ // Connect to REST API (ConnectToRestApi will start web server if needed)
await ConnectToRestApi();
if (isConnected)
{
@@ -313,6 +332,15 @@ namespace RadioDJViewer
// Disconnect
isConnected = false;
apiTimer.Stop();
+ // Stop widget server when disconnecting
+ try
+ {
+ widgetServer?.Stop();
+ widgetServer = null;
+ widgetRunning = false;
+ }
+ catch { }
+ ClearStatusMessage();
UpdateStatusBar();
}
@@ -348,7 +376,17 @@ namespace RadioDJViewer
{
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
{
@@ -501,12 +539,105 @@ namespace RadioDJViewer
private void UpdateStatusBar()
{
- // Status icon
- toolStripStatusLabel2.Image = isConnected ? Properties.Resources.green : Properties.Resources.red;
+ // 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}";
- // Song update notification (example)
- toolStripStatusLabel3.Text = isConnected ? "Connected to API" : "Disconnected";
+ // 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)
diff --git a/RadioDJViewer/ProfileStorage.cs b/RadioDJViewer/ProfileStorage.cs
index 7540234..34797f6 100644
--- a/RadioDJViewer/ProfileStorage.cs
+++ b/RadioDJViewer/ProfileStorage.cs
@@ -20,6 +20,9 @@ namespace RadioDJViewer
public string MarqueeScrollSpeed { get; set; } // "Very Slow", etc.
public int MarqueePauseTime { get; set; } // in seconds
public string MarqueeSeparator { get; set; } // e.g. " | "
+ // Web server settings
+ public bool WebServerEnabled { get; set; }
+ public int WebServerPort { get; set; }
}
public static class ProfileStorage
diff --git a/RadioDJViewer/Properties/AssemblyInfo.cs b/RadioDJViewer/Properties/AssemblyInfo.cs
index 9ba2db6..cb97412 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.5.0")]
-[assembly: AssemblyFileVersion("1.5.0")]
+[assembly: AssemblyVersion("1.8.0")]
+[assembly: AssemblyFileVersion("1.8.0")]
diff --git a/RadioDJViewer/RadioDJViewer.csproj b/RadioDJViewer/RadioDJViewer.csproj
index d148bf7..051525c 100644
--- a/RadioDJViewer/RadioDJViewer.csproj
+++ b/RadioDJViewer/RadioDJViewer.csproj
@@ -68,6 +68,7 @@
+
Form
@@ -83,6 +84,7 @@
+
radiodj-restapi-template.cs
@@ -124,6 +126,9 @@
+
+
+
diff --git a/RadioDJViewer/WidgetServer.cs b/RadioDJViewer/WidgetServer.cs
new file mode 100644
index 0000000..0b5e316
--- /dev/null
+++ b/RadioDJViewer/WidgetServer.cs
@@ -0,0 +1,207 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace RadioDJViewer
+{
+ ///
+ /// Simple embedded-resource-backed HTTP widget server.
+ /// Serves:
+ /// - / or /index.html -> embedded index.html with {{ARTIST}} and {{TITLE}} replaced
+ /// - /style.css -> embedded style.css
+ /// - /script.js -> embedded script.js
+ /// - /albumart -> the current album art image bytes (uses provided delegate to locate file)
+ /// Runs on a background Task and can be stopped via Stop().
+ ///
+ public class WidgetServer
+ {
+ private readonly int port;
+ private readonly Func getArtist;
+ private readonly Func getTitle;
+ private readonly Func getAlbumArtPath;
+ private readonly HttpListener listener;
+ private CancellationTokenSource cts;
+
+ private readonly string htmlTemplate;
+ private readonly string cssText;
+ private readonly string jsText;
+
+ public WidgetServer(int port, Func getArtist, Func getTitle, Func getAlbumArtPath)
+ {
+ this.port = port;
+ this.getArtist = getArtist ?? (() => string.Empty);
+ this.getTitle = getTitle ?? (() => string.Empty);
+ this.getAlbumArtPath = getAlbumArtPath ?? (() => string.Empty);
+ listener = new HttpListener();
+ listener.Prefixes.Add($"http://+:{port}/");
+
+ // Preload embedded resources
+ htmlTemplate = EmbeddedResourceHelper.Read("index.html");
+ cssText = EmbeddedResourceHelper.Read("style.css");
+ jsText = EmbeddedResourceHelper.Read("script.js");
+ }
+
+ public void Start()
+ {
+ if (cts != null) return; // already started
+ cts = new CancellationTokenSource();
+ listener.Start();
+ Task.Run(() => RunAsync(cts.Token));
+ }
+
+ public void Stop()
+ {
+ try
+ {
+ cts?.Cancel();
+ listener.Stop();
+ }
+ catch { }
+ finally
+ {
+ cts = null;
+ }
+ }
+
+ private async Task RunAsync(CancellationToken token)
+ {
+ while (!token.IsCancellationRequested)
+ {
+ HttpListenerContext ctx = null;
+ try
+ {
+ ctx = await listener.GetContextAsync().ConfigureAwait(false);
+ }
+ catch (HttpListenerException)
+ {
+ // listener closed
+ break;
+ }
+ catch (ObjectDisposedException)
+ {
+ break;
+ }
+ catch (Exception)
+ {
+ continue;
+ }
+
+ _ = Task.Run(() => HandleContext(ctx), token);
+ }
+ }
+
+ private void HandleContext(HttpListenerContext ctx)
+ {
+ try
+ {
+ var req = ctx.Request;
+ var resp = ctx.Response;
+ string path = req.Url.AbsolutePath.TrimEnd('/');
+ if (string.IsNullOrEmpty(path) || path == "") path = "/";
+
+ if (path == "/" || path.Equals("/index.html", StringComparison.OrdinalIgnoreCase))
+ {
+ var html = htmlTemplate ?? "";
+ html = html.Replace("{{ARTIST}}", WebUtility.HtmlEncode(getArtist()))
+ .Replace("{{TITLE}}", WebUtility.HtmlEncode(getTitle()));
+ 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("/style.css", StringComparison.OrdinalIgnoreCase))
+ {
+ var buf = Encoding.UTF8.GetBytes(cssText ?? "");
+ resp.ContentType = "text/css; charset=utf-8";
+ resp.ContentLength64 = buf.Length;
+ resp.OutputStream.Write(buf, 0, buf.Length);
+ }
+ else if (path.Equals("/script.js", StringComparison.OrdinalIgnoreCase))
+ {
+ var buf = Encoding.UTF8.GetBytes(jsText ?? "");
+ resp.ContentType = "application/javascript; charset=utf-8";
+ resp.ContentLength64 = buf.Length;
+ resp.OutputStream.Write(buf, 0, buf.Length);
+ }
+ else if (path.Equals("/albumart", StringComparison.OrdinalIgnoreCase))
+ {
+ var imagePath = getAlbumArtPath();
+ if (!string.IsNullOrEmpty(imagePath) && File.Exists(imagePath))
+ {
+ try
+ {
+ byte[] imgBytes = File.ReadAllBytes(imagePath);
+ resp.ContentType = GetMimeTypeFromPath(imagePath);
+ resp.ContentLength64 = imgBytes.Length;
+ resp.OutputStream.Write(imgBytes, 0, imgBytes.Length);
+ }
+ catch
+ {
+ ServeFallbackImage(resp);
+ }
+ }
+ else
+ {
+ ServeFallbackImage(resp);
+ }
+ }
+ else
+ {
+ resp.StatusCode = 404;
+ var msg = Encoding.UTF8.GetBytes("Not found");
+ resp.OutputStream.Write(msg, 0, msg.Length);
+ }
+ resp.OutputStream.Close();
+ }
+ catch
+ {
+ try { ctx.Response.OutputStream.Close(); } catch { }
+ }
+ }
+
+ private void ServeFallbackImage(HttpListenerResponse resp)
+ {
+ try
+ {
+ var img = Properties.Resources.fallback;
+ if (img != null)
+ {
+ using (var ms = new MemoryStream())
+ {
+ img.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
+ var buf = ms.ToArray();
+ resp.ContentType = "image/png";
+ resp.ContentLength64 = buf.Length;
+ resp.OutputStream.Write(buf, 0, buf.Length);
+ }
+ }
+ else
+ {
+ resp.StatusCode = 404;
+ var msg = Encoding.UTF8.GetBytes("No image");
+ resp.OutputStream.Write(msg, 0, msg.Length);
+ }
+ }
+ catch
+ {
+ resp.StatusCode = 500;
+ }
+ }
+
+ private string GetMimeTypeFromPath(string path)
+ {
+ var ext = Path.GetExtension(path).ToLowerInvariant();
+ switch (ext)
+ {
+ case ".png": return "image/png";
+ case ".jpg":
+ case ".jpeg": return "image/jpeg";
+ case ".gif": return "image/gif";
+ default: return "application/octet-stream";
+ }
+ }
+ }
+}
diff --git a/RadioDJViewer/index.html b/RadioDJViewer/index.html
new file mode 100644
index 0000000..8e7abb8
--- /dev/null
+++ b/RadioDJViewer/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Radio DJ Now Playing
+
+
+
+
+
+
+
+
diff --git a/RadioDJViewer/radiodj-restapi-template.Designer.cs b/RadioDJViewer/radiodj-restapi-template.Designer.cs
index 9085bd8..83195f2 100644
--- a/RadioDJViewer/radiodj-restapi-template.Designer.cs
+++ b/RadioDJViewer/radiodj-restapi-template.Designer.cs
@@ -64,9 +64,15 @@
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.groupBox1.SuspendLayout();
this.groupBox3.SuspendLayout();
this.groupBox2.SuspendLayout();
+ this.web_server_settings_groupbox.SuspendLayout();
this.SuspendLayout();
//
// groupBox1
@@ -98,7 +104,7 @@
//
// ok_button_master
//
- this.ok_button_master.Location = new System.Drawing.Point(35, 584);
+ this.ok_button_master.Location = new System.Drawing.Point(65, 648);
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;
@@ -107,7 +113,7 @@
//
// button2
//
- this.button2.Location = new System.Drawing.Point(357, 584);
+ this.button2.Location = new System.Drawing.Point(305, 648);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(157, 23);
this.button2.TabIndex = 2;
@@ -181,7 +187,7 @@
//
// button3
//
- this.button3.Location = new System.Drawing.Point(90, 419);
+ this.button3.Location = new System.Drawing.Point(71, 491);
this.button3.Name = "button3";
this.button3.Size = new System.Drawing.Size(120, 30);
this.button3.TabIndex = 8;
@@ -190,7 +196,7 @@
//
// button4
//
- this.button4.Location = new System.Drawing.Point(305, 419);
+ this.button4.Location = new System.Drawing.Point(316, 491);
this.button4.Name = "button4";
this.button4.Size = new System.Drawing.Size(120, 30);
this.button4.TabIndex = 9;
@@ -203,7 +209,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(20, 298);
+ this.groupBox3.Location = new System.Drawing.Point(31, 370);
this.groupBox3.Name = "groupBox3";
this.groupBox3.Size = new System.Drawing.Size(482, 115);
this.groupBox3.TabIndex = 12;
@@ -296,6 +302,7 @@
//
// groupBox2
//
+ this.groupBox2.Controls.Add(this.web_server_settings_groupbox);
this.groupBox2.Controls.Add(this.label13);
this.groupBox2.Controls.Add(this.comboBoxScrollSpeed);
this.groupBox2.Controls.Add(this.textBoxPauseTime);
@@ -322,7 +329,7 @@
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, 470);
+ this.groupBox2.Size = new System.Drawing.Size(519, 527);
this.groupBox2.TabIndex = 3;
this.groupBox2.TabStop = false;
this.groupBox2.Text = "Profiles";
@@ -400,11 +407,59 @@
this.label12.TabIndex = 27;
this.label12.Text = "Separating character format";
//
+ // 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;
+ //
+ // 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;
+ //
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(543, 619);
+ this.ClientSize = new System.Drawing.Size(572, 695);
this.Controls.Add(this.groupBox2);
this.Controls.Add(this.button2);
this.Controls.Add(this.ok_button_master);
@@ -418,6 +473,8 @@
this.groupBox3.PerformLayout();
this.groupBox2.ResumeLayout(false);
this.groupBox2.PerformLayout();
+ this.web_server_settings_groupbox.ResumeLayout(false);
+ this.web_server_settings_groupbox.PerformLayout();
this.ResumeLayout(false);
}
@@ -461,5 +518,10 @@
private System.Windows.Forms.TextBox textBoxPauseTime;
private System.Windows.Forms.TextBox separatingcharacterformatbox;
private System.Windows.Forms.Label label13;
+ private System.Windows.Forms.GroupBox web_server_settings_groupbox;
+ private System.Windows.Forms.Label web_server_label;
+ 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;
}
}
\ No newline at end of file
diff --git a/RadioDJViewer/radiodj-restapi-template.cs b/RadioDJViewer/radiodj-restapi-template.cs
index c32be81..1dd75d9 100644
--- a/RadioDJViewer/radiodj-restapi-template.cs
+++ b/RadioDJViewer/radiodj-restapi-template.cs
@@ -78,6 +78,13 @@ namespace RadioDJViewer
comboBoxScrollSpeed.SelectedItem = profile.MarqueeScrollSpeed ?? "Medium";
textBoxPauseTime.Text = profile.MarqueePauseTime > 0 ? profile.MarqueePauseTime.ToString() : "6";
separatingcharacterformatbox.Text = profile.MarqueeSeparator ?? " | ";
+ // Web server settings
+ try
+ {
+ web_server_checkbox.Checked = profile.WebServerEnabled;
+ web_server_port_textbox.Text = (profile.WebServerPort > 0 ? profile.WebServerPort : 8080).ToString();
+ }
+ catch { }
}
}
}
@@ -129,7 +136,10 @@ namespace RadioDJViewer
PollingRateSeconds = pollingRate,
MarqueeScrollSpeed = comboBoxScrollSpeed.SelectedItem?.ToString() ?? "Medium",
MarqueePauseTime = int.TryParse(textBoxPauseTime.Text, out int pt) ? pt : 6,
- MarqueeSeparator = separatingcharacterformatbox.Text
+ MarqueeSeparator = separatingcharacterformatbox.Text,
+ // Web server settings
+ WebServerEnabled = web_server_checkbox.Checked,
+ WebServerPort = int.TryParse(web_server_port_textbox.Text, out int wp) ? wp : 8080
};
ProfileStorage.SaveProfile(profile);
// Refresh profile list
diff --git a/RadioDJViewer/script.js b/RadioDJViewer/script.js
new file mode 100644
index 0000000..cb437ae
--- /dev/null
+++ b/RadioDJViewer/script.js
@@ -0,0 +1,128 @@
+///////////////
+// PARAMETERS //
+///////////////
+
+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;
+ }
+
+ 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);
+ }
+
+ if (artist !== currentArtist) {
+ currentArtist = artist;
+ artistElement.classList.add("updating");
+ artistElement.textContent = artist || "Unknown Artist";
+ trackChanged = true;
+ setTimeout(() => {
+ artistElement.classList.remove("updating");
+ }, 400);
+ }
+
+ // Return whether track changed (to trigger image update)
+ return trackChanged;
+}
+
+/**
+ * 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();
+ }
+ }
+}
+
+///////////////
+// INITIALIZE //
+///////////////
+
+function init() {
+ // Load initial data immediately
+ refresh();
+
+ // Set up refresh interval based on URL parameter or default
+ refreshInterval = setInterval(refresh, refreshRate * 1000);
+
+ console.log(`Radio DJ widget initialized - refreshing every ${refreshRate}s`);
+}
+
+// Start when DOM is ready
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+} else {
+ init();
+}
diff --git a/RadioDJViewer/style.css b/RadioDJViewer/style.css
new file mode 100644
index 0000000..ecfa75f
--- /dev/null
+++ b/RadioDJViewer/style.css
@@ -0,0 +1,143 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ background: #000;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
+}
+
+/* Main widget container */
+.widget-container {
+ position: relative;
+ display: flex;
+ width: 100%;
+ max-width: 650px;
+ aspect-ratio: auto;
+ height: 200px;
+ border-radius: 20px;
+ overflow: hidden;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.95);
+ margin: 20px;
+}
+
+/* Blurred background layer (z-index: 0) */
+.background-blur {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ overflow: hidden;
+}
+
+.background-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ filter: blur(40px) brightness(0.6);
+}
+
+/* Dark overlay (z-index: 1) */
+.overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 1;
+ pointer-events: none;
+}
+
+/* Content layer (z-index: 2) */
+.widget-content {
+ position: relative;
+ display: flex;
+ gap: 24px;
+ padding: 20px;
+ z-index: 2;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+}
+
+/* Album art box (left) */
+.album-art-box {
+ flex-shrink: 0;
+ width: 140px;
+ height: 140px;
+ border-radius: 10px;
+ overflow: hidden;
+ background: rgba(60, 60, 60, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6);
+}
+
+.album-art {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+/* Song info box (right) */
+.song-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 4px;
+ min-width: 0;
+ padding: 0 12px;
+}
+
+/* Song title */
+.song-title {
+ font-size: 30px;
+ font-weight: 700;
+ color: #ffffff;
+ text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.9);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ letter-spacing: -0.5px;
+ line-height: 1.1;
+}
+
+/* Song artist */
+.song-artist {
+ font-size: 22px;
+ font-weight: 500;
+ color: #ffffff;
+ text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.9);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ letter-spacing: -0.3px;
+ line-height: 1.1;
+ opacity: 1;
+}
+
+/* Smooth fade animation for updates */
+@keyframes fadeIn {
+ from {
+ opacity: 0.5;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.song-title.updating,
+.song-artist.updating {
+ animation: fadeIn 0.4s ease-in-out;
+}