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 + + + +
+ +
+ Album backdrop +
+ + +
+ + +
+ +
+ Album art +
+ + +
+
Loading...
+
Loading...
+
+
+
+ + + + 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; +}