diff --git a/README.md b/README.md index 8e2ade8..5677fee 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A Windows companion suite for [Moltbot](https://moltbot.com) - the AI-powered personal assistant. +![Molty - Windows Tray App](docs/molty1.png) + ## Projects This monorepo contains three projects: @@ -29,19 +31,30 @@ dotnet build dotnet run --project src/Moltbot.Tray ``` -## ๐Ÿ“ฆ Moltbot.Tray +## ๐Ÿ“ฆ Moltbot.Tray (Molty) -Windows system tray companion that connects to your local Moltbot gateway. +Modern Windows 11-style system tray companion that connects to your local Moltbot gateway. ### Features -- ๐Ÿฆž Lobster icon in system tray (connected/disconnected states) -- ๐Ÿ’ฌ Quick Send - Send messages via global hotkey (Ctrl+Alt+Shift+C) -- ๐Ÿ”„ Auto-updates from GitHub Releases -- ๐ŸŒ Web Chat - Embedded chat window -- ๐Ÿ“Š Status Display - View sessions and channels -- ๐Ÿ”” Toast Notifications - Clickable Windows notifications -- ๐Ÿš€ Auto-start with Windows -- โš™๏ธ Settings management +- ๐Ÿฆž **Lobster branding** - Pixel-art lobster tray icon with status colors +- ๐ŸŽจ **Modern UI** - Windows 11 flyout menu with dark/light mode support +- ๐Ÿ’ฌ **Quick Send** - Send messages via global hotkey (Ctrl+Alt+Shift+C) +- ๐Ÿ”„ **Auto-updates** - Automatic updates from GitHub Releases +- ๐ŸŒ **Web Chat** - Embedded chat window with WebView2 +- ๐Ÿ“Š **Live Status** - Real-time sessions, channels, and usage display +- ๐Ÿ”” **Toast Notifications** - Clickable Windows notifications with filters +- ๐Ÿ“ก **Channel Control** - Start/stop Telegram & WhatsApp from the menu +- โฑ **Cron Jobs** - Quick access to scheduled tasks +- ๐Ÿš€ **Auto-start** - Launch with Windows +- โš™๏ธ **Settings** - Full configuration dialog +- ๐ŸŽฏ **First-run experience** - Welcome dialog guides new users + +### Menu Sections +- **Status** - Gateway connection status with click-to-view details +- **Sessions** - Active agent sessions (clickable โ†’ dashboard) +- **Channels** - Telegram/WhatsApp status with toggle control +- **Actions** - Dashboard, Web Chat, Quick Send, Cron Jobs, History +- **Settings** - Configuration, auto-start, logs ### Mac Parity Status @@ -55,6 +68,9 @@ Windows system tray companion that connects to your local Moltbot gateway. | Auto-start | โœ… | โœ… | | Session display | โœ… | โœ… | | Channel health | โœ… | โœ… | +| Channel control | โœ… | โœ… | +| Modern UI styling | โœ… | โœ… | +| Dark/Light mode | โœ… | โœ… | | Deep links | โœ… | ๐Ÿ”„ | ## ๐Ÿ“ฆ Moltbot.CommandPalette @@ -81,6 +97,7 @@ Shared library containing: - `MoltbotGatewayClient` - WebSocket client for gateway protocol - `IMoltbotLogger` - Logging interface - Data models (SessionInfo, ChannelHealth, etc.) +- Channel control (start/stop channels via gateway) ## Development @@ -91,6 +108,8 @@ moltbot-windows-hub/ โ”‚ โ”œโ”€โ”€ Moltbot.Shared/ # Shared gateway library โ”‚ โ”œโ”€โ”€ Moltbot.Tray/ # System tray app โ”‚ โ””โ”€โ”€ Moltbot.CommandPalette/ # PowerToys extension +โ”œโ”€โ”€ docs/ +โ”‚ โ””โ”€โ”€ molty1.png # Screenshot โ”œโ”€โ”€ moltbot-windows-hub.sln โ”œโ”€โ”€ README.md โ”œโ”€โ”€ LICENSE @@ -105,6 +124,13 @@ Settings are stored in: Default gateway: `ws://localhost:18789` +### First Run + +On first run without a token, Molty displays a welcome dialog that: +1. Explains what's needed to get started +2. Links to [documentation](https://docs.molt.bot/web/dashboard) for token setup +3. Opens Settings to configure the connection + ## License MIT License - see [LICENSE](LICENSE) diff --git a/docs/molty1.png b/docs/molty1.png new file mode 100644 index 0000000..034b544 Binary files /dev/null and b/docs/molty1.png differ diff --git a/src/Moltbot.Shared/MoltbotGatewayClient.cs b/src/Moltbot.Shared/MoltbotGatewayClient.cs index 527f178..4222be9 100644 --- a/src/Moltbot.Shared/MoltbotGatewayClient.cs +++ b/src/Moltbot.Shared/MoltbotGatewayClient.cs @@ -163,6 +163,54 @@ public class MoltbotGatewayClient : IDisposable catch { } } + /// Start a channel (telegram, whatsapp, etc). + public async Task StartChannelAsync(string channelName) + { + if (_webSocket?.State != WebSocketState.Open) return false; + try + { + var req = new + { + type = "req", + id = Guid.NewGuid().ToString(), + method = "channel.start", + @params = new { channel = channelName } + }; + await SendRawAsync(JsonSerializer.Serialize(req)); + _logger.Info($"Sent channel.start for {channelName}"); + return true; + } + catch (Exception ex) + { + _logger.Error($"Failed to start channel {channelName}", ex); + return false; + } + } + + /// Stop a channel (telegram, whatsapp, etc). + public async Task StopChannelAsync(string channelName) + { + if (_webSocket?.State != WebSocketState.Open) return false; + try + { + var req = new + { + type = "req", + id = Guid.NewGuid().ToString(), + method = "channel.stop", + @params = new { channel = channelName } + }; + await SendRawAsync(JsonSerializer.Serialize(req)); + _logger.Info($"Sent channel.stop for {channelName}"); + return true; + } + catch (Exception ex) + { + _logger.Error($"Failed to stop channel {channelName}", ex); + return false; + } + } + // --- Connection management --- private async Task ReconnectWithBackoffAsync() @@ -594,6 +642,8 @@ public class MoltbotGatewayClient : IDisposable bool isConfigured = false; bool isLinked = false; bool probeOk = false; + bool hasError = false; + string? tokenSource = null; if (val.TryGetProperty("running", out var running)) isRunning = running.GetBoolean(); @@ -607,18 +657,27 @@ public class MoltbotGatewayClient : IDisposable // Check probe status for webhook-based channels like Telegram if (val.TryGetProperty("probe", out var probe) && probe.TryGetProperty("ok", out var ok)) probeOk = ok.GetBoolean(); + // Check for errors + if (val.TryGetProperty("lastError", out var lastError) && lastError.ValueKind != JsonValueKind.Null) + hasError = true; + // Check token source (for Telegram - if configured, bot token was validated) + if (val.TryGetProperty("tokenSource", out var ts)) + tokenSource = ts.GetString(); - // Determine status string + // Determine status string - unified for parity between channels + // Key insight: if configured=true and no errors, the channel is ready + // - WhatsApp: linked=true means authenticated + // - Telegram: configured=true means bot token was validated if (val.TryGetProperty("status", out var status)) ch.Status = status.GetString() ?? "unknown"; + else if (hasError) + ch.Status = "error"; else if (isRunning) ch.Status = "running"; - else if (probeOk && isConfigured) - ch.Status = "ready"; // Webhook mode, bot is responding - else if (isLinked) - ch.Status = "linked"; // Authenticated but not running - else if (isConfigured) - ch.Status = "stopped"; + else if (isConfigured && (probeOk || isLinked)) + ch.Status = "ready"; // Explicitly verified ready + else if (isConfigured && !hasError) + ch.Status = "ready"; // Configured without errors = ready (token was validated at config time) else ch.Status = "not configured"; diff --git a/src/Moltbot.Tray/DownloadProgressDialog.cs b/src/Moltbot.Tray/DownloadProgressDialog.cs index b8fd8a8..b4c3cfa 100644 --- a/src/Moltbot.Tray/DownloadProgressDialog.cs +++ b/src/Moltbot.Tray/DownloadProgressDialog.cs @@ -6,7 +6,7 @@ using Updatum; namespace MoltbotTray; -public class DownloadProgressDialog : Form +public class DownloadProgressDialog : ModernForm { private readonly UpdatumManager _updater; private readonly ProgressBar _progressBar; @@ -17,41 +17,26 @@ public class DownloadProgressDialog : Form _updater = updater; _updater.PropertyChanged += UpdaterOnPropertyChanged; - Text = "Downloading Update - Moltbot Tray"; - Size = new Size(400, 150); - StartPosition = FormStartPosition.CenterScreen; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; - ControlBox = false; // No close button during download - Icon = SystemIcons.Information; + Text = "Downloading Update โ€” Moltbot Tray"; + Size = new Size(420, 160); + ControlBox = false; + Icon = IconHelper.GetLobsterIcon(); - var titleLabel = new Label - { - Text = "๐Ÿฆž Downloading update...", - Font = new Font(Font.FontFamily, 10, FontStyle.Bold), - Location = new Point(20, 20), - AutoSize = true - }; + var titleLabel = CreateModernLabel("๐Ÿฆž Downloading update..."); + titleLabel.Font = new Font("Segoe UI", 11, FontStyle.Bold); + titleLabel.ForeColor = AccentColor; + titleLabel.Location = new Point(20, 20); Controls.Add(titleLabel); - _progressBar = new ProgressBar - { - Location = new Point(20, 55), - Size = new Size(340, 25), - Minimum = 0, - Maximum = 100, - Style = ProgressBarStyle.Continuous - }; + _progressBar = CreateModernProgressBar(); + _progressBar.Location = new Point(20, 60); + _progressBar.Size = new Size(364, 8); Controls.Add(_progressBar); - _progressLabel = new Label - { - Text = "Starting download...", - Location = new Point(20, 85), - Size = new Size(340, 20), - TextAlign = ContentAlignment.MiddleCenter - }; + _progressLabel = CreateModernLabel("Starting download...", isSubtle: true); + _progressLabel.Location = new Point(20, 78); + _progressLabel.Size = new Size(364, 24); + _progressLabel.TextAlign = ContentAlignment.MiddleCenter; Controls.Add(_progressLabel); } @@ -60,13 +45,9 @@ public class DownloadProgressDialog : Form if (e.PropertyName == nameof(UpdatumManager.DownloadedPercentage)) { if (InvokeRequired) - { Invoke(() => UpdateProgress()); - } else - { UpdateProgress(); - } } } @@ -82,3 +63,4 @@ public class DownloadProgressDialog : Form base.OnFormClosing(e); } } + diff --git a/src/Moltbot.Tray/ModernForm.cs b/src/Moltbot.Tray/ModernForm.cs new file mode 100644 index 0000000..d554977 --- /dev/null +++ b/src/Moltbot.Tray/ModernForm.cs @@ -0,0 +1,257 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Microsoft.Win32; + +namespace MoltbotTray; + +/// +/// Base form with Windows 11 modern styling - dark/light mode, rounded corners, Moltbot branding. +/// Inherit from this for consistent look across all dialogs. +/// +public class ModernForm : Form +{ + [DllImport("dwmapi.dll")] + private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + + private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; + private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; + private const int DWMWCP_ROUND = 2; + + // Theme colors - exposed for child controls + protected bool IsDarkMode { get; private set; } + protected Color AccentColor => Color.FromArgb(220, 53, 53); // Lobster red + protected Color BackgroundColor { get; private set; } + protected Color ForegroundColor { get; private set; } + protected Color SurfaceColor { get; private set; } + protected Color BorderColor { get; private set; } + protected Color HoverColor { get; private set; } + protected Color SubtleTextColor { get; private set; } + + public ModernForm() + { + DetectTheme(); + + // Base form styling + Font = new Font("Segoe UI", 9.5f); + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + } + + private void DetectTheme() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + var value = key?.GetValue("AppsUseLightTheme"); + IsDarkMode = value is int i && i == 0; + } + catch + { + IsDarkMode = false; + } + + if (IsDarkMode) + { + BackgroundColor = Color.FromArgb(32, 32, 32); + ForegroundColor = Color.FromArgb(255, 255, 255); + SurfaceColor = Color.FromArgb(45, 45, 48); + BorderColor = Color.FromArgb(60, 60, 60); + HoverColor = Color.FromArgb(55, 55, 58); + SubtleTextColor = Color.FromArgb(180, 180, 180); + } + else + { + BackgroundColor = Color.FromArgb(249, 249, 249); + ForegroundColor = Color.FromArgb(28, 28, 28); + SurfaceColor = Color.FromArgb(255, 255, 255); + BorderColor = Color.FromArgb(200, 200, 200); + HoverColor = Color.FromArgb(229, 229, 229); + SubtleTextColor = Color.FromArgb(100, 100, 100); + } + + BackColor = BackgroundColor; + ForeColor = ForegroundColor; + } + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + ApplyModernStyling(); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + // Apply theme colors to all child controls + ApplyThemeToControls(Controls); + } + + private void ApplyThemeToControls(Control.ControlCollection controls) + { + foreach (Control ctrl in controls) + { + // Skip controls that have explicit colors set (like accent-colored labels) + if (ctrl.ForeColor == AccentColor) continue; + + // Apply foreground color to labels and checkboxes + if (ctrl is Label || ctrl is CheckBox || ctrl is RadioButton) + { + if (ctrl.ForeColor == Color.Black || ctrl.ForeColor == SystemColors.ControlText) + ctrl.ForeColor = ForegroundColor; + } + + // Recurse into containers + if (ctrl.HasChildren) + ApplyThemeToControls(ctrl.Controls); + } + } + + private void ApplyModernStyling() + { + // Enable Windows 11 rounded corners + int preference = DWMWCP_ROUND; + DwmSetWindowAttribute(Handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref preference, sizeof(int)); + + // Enable dark mode title bar + int darkMode = IsDarkMode ? 1 : 0; + DwmSetWindowAttribute(Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref darkMode, sizeof(int)); + } + + /// + /// Creates a styled button with Moltbot branding. + /// + protected Button CreateModernButton(string text, bool isPrimary = false) + { + var btn = new Button + { + Text = text, + FlatStyle = FlatStyle.Flat, + Font = new Font("Segoe UI", 9.5f, isPrimary ? FontStyle.Bold : FontStyle.Regular), + Cursor = Cursors.Hand, + Height = 32, + Padding = new Padding(12, 0, 12, 0) + }; + + if (isPrimary) + { + btn.BackColor = AccentColor; + btn.ForeColor = Color.White; + btn.FlatAppearance.BorderSize = 0; + btn.FlatAppearance.MouseOverBackColor = Color.FromArgb(200, 43, 43); + } + else + { + btn.BackColor = SurfaceColor; + btn.ForeColor = ForegroundColor; + btn.FlatAppearance.BorderColor = BorderColor; + btn.FlatAppearance.BorderSize = 1; + btn.FlatAppearance.MouseOverBackColor = HoverColor; + } + + return btn; + } + + /// + /// Creates a styled text box. + /// + protected TextBox CreateModernTextBox() + { + return new TextBox + { + Font = new Font("Segoe UI", 10f), + BackColor = SurfaceColor, + ForeColor = ForegroundColor, + BorderStyle = BorderStyle.FixedSingle + }; + } + + /// + /// Creates a styled label. + /// + protected Label CreateModernLabel(string text, bool isSubtle = false) + { + return new Label + { + Text = text, + Font = new Font("Segoe UI", 9.5f), + ForeColor = isSubtle ? SubtleTextColor : ForegroundColor, + AutoSize = true + }; + } + + /// + /// Creates a styled checkbox. + /// + protected CheckBox CreateModernCheckBox(string text) + { + var cb = new CheckBox + { + Text = text, + Font = new Font("Segoe UI", 9.5f), + ForeColor = ForegroundColor, + BackColor = Color.Transparent, + AutoSize = true, + FlatStyle = FlatStyle.Standard + }; + return cb; + } + + /// + /// Creates a styled group box. + /// + protected GroupBox CreateModernGroupBox(string text) + { + return new GroupBox + { + Text = text, + Font = new Font("Segoe UI", 9.5f, FontStyle.Bold), + ForeColor = AccentColor, + BackColor = Color.Transparent + }; + } + + /// + /// Creates a styled panel with border. + /// + protected Panel CreateModernPanel() + { + return new Panel + { + BackColor = SurfaceColor, + BorderStyle = BorderStyle.None, + Padding = new Padding(12) + }; + } + + /// + /// Creates a horizontal separator line. + /// + protected Panel CreateSeparator() + { + return new Panel + { + Height = 1, + BackColor = BorderColor, + Dock = DockStyle.Top, + Margin = new Padding(0, 8, 0, 8) + }; + } + + /// + /// Creates a styled progress bar. + /// + protected ProgressBar CreateModernProgressBar() + { + return new ProgressBar + { + Style = ProgressBarStyle.Continuous, + Height = 6, + ForeColor = AccentColor + }; + } +} diff --git a/src/Moltbot.Tray/ModernTrayMenu.cs b/src/Moltbot.Tray/ModernTrayMenu.cs new file mode 100644 index 0000000..7655bb2 --- /dev/null +++ b/src/Moltbot.Tray/ModernTrayMenu.cs @@ -0,0 +1,435 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Microsoft.Win32; + +namespace MoltbotTray; + +/// +/// Modern flyout menu with Windows 11 styling - dark/light mode, rounded corners, acrylic blur. +/// Replaces the dated ContextMenuStrip with a custom-drawn popup. +/// +public class ModernTrayMenu : Form +{ + // DWM APIs for acrylic/mica effect + [DllImport("dwmapi.dll")] + private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + + [DllImport("dwmapi.dll")] + private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset); + + [StructLayout(LayoutKind.Sequential)] + private struct MARGINS { public int Left, Right, Top, Bottom; } + + private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; + private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; + private const int DWMWA_SYSTEMBACKDROP_TYPE = 38; + private const int DWMWCP_ROUND = 2; + private const int DWMSBT_TRANSIENTWINDOW = 3; // Acrylic + + // Theme colors + private bool _isDarkMode; + private Color _backgroundColor; + private Color _foregroundColor; + private Color _hoverColor; + private Color _accentColor; + private Color _separatorColor; + private Color _subtleTextColor; + + // Menu items + private readonly List _items = new(); + private int _hoveredIndex = -1; + private const int ItemHeight = 36; + private const int IconWidth = 32; // Wider for emoji + private const int Padding = 16; // More padding + private const int CornerRadius = 8; + + public event EventHandler? MenuItemClicked; + + public ModernTrayMenu() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); + + FormBorderStyle = FormBorderStyle.None; + ShowInTaskbar = false; + TopMost = true; + StartPosition = FormStartPosition.Manual; + + // Detect theme (styling applied in OnHandleCreated) + DetectTheme(); + + // Track mouse for hover effects + MouseMove += OnMouseMove; + MouseLeave += (_, _) => { _hoveredIndex = -1; Invalidate(); }; + MouseClick += OnMouseClick; + + // Close when clicking outside + Deactivate += (_, _) => Hide(); + } + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + ApplyModernStyling(); + } + + private void DetectTheme() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + var value = key?.GetValue("AppsUseLightTheme"); + _isDarkMode = value is int i && i == 0; + } + catch + { + _isDarkMode = false; + } + + if (_isDarkMode) + { + _backgroundColor = Color.FromArgb(32, 32, 32); + _foregroundColor = Color.FromArgb(255, 255, 255); + _hoverColor = Color.FromArgb(45, 45, 48); + _accentColor = Color.FromArgb(255, 99, 71); // Lobster red + _separatorColor = Color.FromArgb(80, 80, 80); + _subtleTextColor = Color.FromArgb(180, 180, 180); + } + else + { + _backgroundColor = Color.FromArgb(249, 249, 249); + _foregroundColor = Color.FromArgb(28, 28, 28); + _hoverColor = Color.FromArgb(229, 229, 229); + _accentColor = Color.FromArgb(220, 53, 53); // Lobster red + _separatorColor = Color.FromArgb(200, 200, 200); + _subtleTextColor = Color.FromArgb(100, 100, 100); + } + + BackColor = _backgroundColor; + } + + private void ApplyModernStyling() + { + // Enable Windows 11 rounded corners + int preference = DWMWCP_ROUND; + DwmSetWindowAttribute(Handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref preference, sizeof(int)); + + // Enable dark mode for title bar (affects some rendering) + int darkMode = _isDarkMode ? 1 : 0; + DwmSetWindowAttribute(Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref darkMode, sizeof(int)); + + // Try to enable acrylic backdrop (Windows 11 22H2+) + int backdropType = DWMSBT_TRANSIENTWINDOW; + DwmSetWindowAttribute(Handle, DWMWA_SYSTEMBACKDROP_TYPE, ref backdropType, sizeof(int)); + } + + public void ClearItems() => _items.Clear(); + + public void AddBrandHeader(string icon, string text) + { + _items.Add(new ModernMenuItem + { + Id = "", + Icon = icon, + Text = text, + Enabled = false, + IsHeader = true, + IsBrandHeader = true, + IsSeparator = false + }); + } + + public void AddItem(string id, string icon, string text, bool enabled = true, bool isHeader = false) + { + _items.Add(new ModernMenuItem + { + Id = id, + Icon = icon, + Text = text, + Enabled = enabled, + IsHeader = isHeader, + IsSeparator = false + }); + } + + public void AddSeparator() + { + _items.Add(new ModernMenuItem { IsSeparator = true }); + } + + public void AddStatusItem(string id, string icon, string text, string status, Color statusColor) + { + _items.Add(new ModernMenuItem + { + Id = id, + Icon = icon, + Text = text, + Status = status, + StatusColor = statusColor, + Enabled = true + }); + } + + public void ShowAtCursor() + { + // Calculate size + int width = 320; // Wider for better spacing + int height = Padding * 2; + foreach (var item in _items) + { + if (item.IsSeparator) + height += 9; + else if (item.IsBrandHeader) + height += 48; // Big brand header + else if (item.IsHeader) + height += 32; + else + height += ItemHeight; + } + + // Minimum height if no items + if (height < 50) height = 50; + + Size = new Size(width, height); + + // Position near cursor, but keep on screen + var cursor = Cursor.Position; + var screen = Screen.FromPoint(cursor).WorkingArea; + + int x = cursor.X - width / 2; + int y = cursor.Y - height - 10; + + // Adjust if off screen + if (x < screen.Left) x = screen.Left + 8; + if (x + width > screen.Right) x = screen.Right - width - 8; + if (y < screen.Top) y = cursor.Y + 20; // Show below cursor instead + if (y + height > screen.Bottom) y = screen.Bottom - height - 8; + + Location = new Point(x, y); + Show(); + Activate(); + Invalidate(); // Force repaint + } + + protected override void OnPaint(PaintEventArgs e) + { + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + + // Draw rounded background + using var bgBrush = new SolidBrush(_backgroundColor); + using var path = CreateRoundedRectangle(ClientRectangle, CornerRadius); + g.FillPath(bgBrush, path); + + // Draw border + using var borderPen = new Pen(Color.FromArgb(_isDarkMode ? 50 : 30, _isDarkMode ? 255 : 0, _isDarkMode ? 255 : 0, _isDarkMode ? 255 : 0), 1); + g.DrawPath(borderPen, path); + + // Draw items + int y = Padding; + for (int i = 0; i < _items.Count; i++) + { + var item = _items[i]; + + if (item.IsSeparator) + { + // Draw separator line + using var sepPen = new Pen(_separatorColor, 1); + g.DrawLine(sepPen, Padding, y + 4, Width - Padding, y + 4); + y += 9; + continue; + } + + int itemHeight; + if (item.IsBrandHeader) + itemHeight = 48; + else if (item.IsHeader) + itemHeight = 32; + else + itemHeight = ItemHeight; + + var itemRect = new Rectangle(8, y, Width - 16, itemHeight); + + // Hover highlight + if (i == _hoveredIndex && item.Enabled && !item.IsHeader) + { + using var hoverBrush = new SolidBrush(_hoverColor); + using var hoverPath = CreateRoundedRectangle(itemRect, 4); + g.FillPath(hoverBrush, hoverPath); + } + + // Icon - special handling for brand header + if (!string.IsNullOrEmpty(item.Icon)) + { + Color iconColor; + float iconFontSize; + string fontName; + int iconWidth; + + if (item.IsBrandHeader) + { + iconColor = _accentColor; + iconFontSize = 26; // Big lobster! + fontName = "Segoe UI Emoji"; // Use emoji font for lobster + iconWidth = 60; // Plenty of room for lobster + } + else if (item.IsHeader) + { + iconColor = _accentColor; + iconFontSize = 14; + fontName = "Segoe UI Symbol"; + iconWidth = IconWidth; + } + else if (!item.Enabled || string.IsNullOrEmpty(item.Id) || item.Id.StartsWith("session:")) + { + iconColor = _subtleTextColor; + iconFontSize = 11; + fontName = "Segoe UI Symbol"; + iconWidth = IconWidth; + } + else + { + iconColor = _accentColor; + iconFontSize = 11; + fontName = "Segoe UI Symbol"; + iconWidth = IconWidth; + } + + using var iconFont = new Font(fontName, iconFontSize); + var iconRect = new Rectangle(Padding, y, iconWidth, itemHeight); + TextRenderer.DrawText(g, item.Icon, iconFont, iconRect, iconColor, + TextFormatFlags.Left | TextFormatFlags.VerticalCenter); + } + + // Text + var textColor = item.IsHeader ? _foregroundColor : (item.Enabled ? _foregroundColor : _subtleTextColor); + var fontSize = item.IsBrandHeader ? 14f : (item.IsHeader ? 10.5f : 9.5f); + var fontStyle = (item.IsHeader || item.IsBrandHeader) ? FontStyle.Bold : FontStyle.Regular; + using var textFont = new Font("Segoe UI", fontSize, fontStyle); + var textX = Padding + (item.IsBrandHeader ? 64 : IconWidth + 4); + // Only reserve space for status badge if item has one + var rightMargin = string.IsNullOrEmpty(item.Status) ? Padding : 70; + var textRect = new Rectangle(textX, y, Width - textX - rightMargin, itemHeight); + TextRenderer.DrawText(g, item.Text, textFont, textRect, textColor, + TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); + + // Status badge (right side) + if (!string.IsNullOrEmpty(item.Status)) + { + using var statusFont = new Font("Segoe UI", 8, FontStyle.Bold); + var statusSize = TextRenderer.MeasureText(item.Status, statusFont); + var statusRect = new Rectangle(Width - Padding - statusSize.Width - 12, y + (itemHeight - 18) / 2, statusSize.Width + 8, 18); + + using var statusBgBrush = new SolidBrush(Color.FromArgb(30, item.StatusColor)); + using var statusPath = CreateRoundedRectangle(statusRect, 4); + g.FillPath(statusBgBrush, statusPath); + + TextRenderer.DrawText(g, item.Status, statusFont, statusRect, item.StatusColor, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); + } + + y += itemHeight; + } + } + + private void OnMouseMove(object? sender, MouseEventArgs e) + { + int y = Padding; + int newHover = -1; + + for (int i = 0; i < _items.Count; i++) + { + var item = _items[i]; + int itemHeight; + if (item.IsSeparator) + itemHeight = 9; + else if (item.IsBrandHeader) + itemHeight = 48; + else if (item.IsHeader) + itemHeight = 32; + else + itemHeight = ItemHeight; + + // Allow hover on non-separators that are either: + // - Not headers and enabled, OR + // - Headers with an ID (clickable headers like Sessions) + var isClickable = !item.IsSeparator && !item.IsBrandHeader && + ((!item.IsHeader && item.Enabled) || (item.IsHeader && !string.IsNullOrEmpty(item.Id))); + + if (isClickable) + { + if (e.Y >= y && e.Y < y + itemHeight) + { + newHover = i; + break; + } + } + y += itemHeight; + } + + if (newHover != _hoveredIndex) + { + _hoveredIndex = newHover; + Cursor = newHover >= 0 ? Cursors.Hand : Cursors.Default; + Invalidate(); + } + } + + private void OnMouseClick(object? sender, MouseEventArgs e) + { + if (_hoveredIndex >= 0 && _hoveredIndex < _items.Count) + { + var item = _items[_hoveredIndex]; + // Allow clicking if enabled, not separator, and either not a header OR a header with an ID + if (item.Enabled && !item.IsSeparator && (!item.IsHeader || !string.IsNullOrEmpty(item.Id))) + { + Hide(); + MenuItemClicked?.Invoke(this, item.Id); + } + } + } + + private static GraphicsPath CreateRoundedRectangle(Rectangle rect, int radius) + { + var path = new GraphicsPath(); + int diameter = radius * 2; + var arc = new Rectangle(rect.X, rect.Y, diameter, diameter); + + path.AddArc(arc, 180, 90); // Top-left + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); // Top-right + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); // Bottom-right + arc.X = rect.Left; + path.AddArc(arc, 90, 90); // Bottom-left + path.CloseFigure(); + + return path; + } + + protected override CreateParams CreateParams + { + get + { + var cp = base.CreateParams; + cp.ClassStyle |= 0x00020000; // CS_DROPSHADOW + return cp; + } + } + + private class ModernMenuItem + { + public string Id { get; set; } = ""; + public string Icon { get; set; } = ""; + public string Text { get; set; } = ""; + public string Status { get; set; } = ""; + public Color StatusColor { get; set; } = Color.Gray; + public bool Enabled { get; set; } = true; + public bool IsSeparator { get; set; } + public bool IsHeader { get; set; } + public bool IsBrandHeader { get; set; } + } +} diff --git a/src/Moltbot.Tray/NotificationHistoryForm.cs b/src/Moltbot.Tray/NotificationHistoryForm.cs index 7ffa390..9cac87d 100644 --- a/src/Moltbot.Tray/NotificationHistoryForm.cs +++ b/src/Moltbot.Tray/NotificationHistoryForm.cs @@ -6,9 +6,9 @@ using System.Windows.Forms; namespace MoltbotTray; /// -/// Shows recent notification history in a simple list view. +/// Shows recent notification history in a modern styled list view. /// -public class NotificationHistoryForm : Form +public class NotificationHistoryForm : ModernForm { private ListView? _listView; private Button _clearButton = null!; @@ -30,12 +30,10 @@ public class NotificationHistoryForm : Form Type = type }); - // Trim old entries while (_history.Count > MaxHistory) _history.RemoveAt(0); } - // If window is open, refresh it _instance?.RefreshList(); } @@ -61,9 +59,9 @@ public class NotificationHistoryForm : Form private void InitializeComponent() { Text = "Notification History โ€” Moltbot Tray"; - Size = new Size(600, 450); - MinimumSize = new Size(400, 300); - StartPosition = FormStartPosition.CenterScreen; + Size = new Size(680, 500); + MinimumSize = new Size(480, 340); + FormBorderStyle = FormBorderStyle.Sizable; Icon = IconHelper.GetLobsterIcon(); _listView = new ListView @@ -71,42 +69,49 @@ public class NotificationHistoryForm : Form Dock = DockStyle.Fill, View = View.Details, FullRowSelect = true, - GridLines = true, - Font = new Font("Segoe UI", 9F) + GridLines = false, + Font = new Font("Segoe UI", 9.5F), + BackColor = SurfaceColor, + ForeColor = ForegroundColor, + BorderStyle = BorderStyle.None }; - _listView.Columns.Add("Time", 130); - _listView.Columns.Add("Type", 80); - _listView.Columns.Add("Title", 150); - _listView.Columns.Add("Message", 300); + _listView.Columns.Add("Time", 140); + _listView.Columns.Add("Type", 85); + _listView.Columns.Add("Title", 160); + _listView.Columns.Add("Message", 320); - var buttonPanel = new FlowLayoutPanel + var buttonPanel = new Panel { Dock = DockStyle.Bottom, - Height = 40, - FlowDirection = FlowDirection.RightToLeft, - Padding = new Padding(5) + Height = 56, + BackColor = SurfaceColor, + Padding = new Padding(16, 12, 16, 12) }; - _closeButton = new Button - { - Text = "&Close", - Size = new Size(75, 26), - Font = new Font("Segoe UI", 9F) - }; + _closeButton = CreateModernButton("Close"); + _closeButton.Size = new Size(90, 36); _closeButton.Click += (_, _) => Close(); - _clearButton = new Button - { - Text = "C&lear All", - Size = new Size(85, 26), - Font = new Font("Segoe UI", 9F) - }; + _clearButton = CreateModernButton("Clear All", isPrimary: true); + _clearButton.Size = new Size(100, 36); _clearButton.Click += (_, _) => { lock (_history) _history.Clear(); RefreshList(); }; + var buttonFlow = new FlowLayoutPanel + { + Dock = DockStyle.Right, + FlowDirection = FlowDirection.RightToLeft, + AutoSize = true, + BackColor = Color.Transparent + }; + buttonFlow.Controls.Add(_closeButton); + buttonFlow.Controls.Add(_clearButton); + + buttonPanel.Controls.Add(buttonFlow); + buttonPanel.Controls.Add(_closeButton); buttonPanel.Controls.Add(_clearButton); @@ -129,7 +134,6 @@ public class NotificationHistoryForm : Form lock (_history) { - // Show newest first for (int i = _history.Count - 1; i >= 0; i--) { var entry = _history[i]; @@ -159,3 +163,4 @@ public class NotificationHistoryForm : Form } } + diff --git a/src/Moltbot.Tray/QuickSendDialog.cs b/src/Moltbot.Tray/QuickSendDialog.cs index efc8ae7..b4dd2b3 100644 --- a/src/Moltbot.Tray/QuickSendDialog.cs +++ b/src/Moltbot.Tray/QuickSendDialog.cs @@ -4,7 +4,7 @@ using System.Windows.Forms; namespace MoltbotTray; -public partial class QuickSendDialog : Form +public partial class QuickSendDialog : ModernForm { private TextBox _messageTextBox = null!; private Button _sendButton = null!; @@ -20,85 +20,52 @@ public partial class QuickSendDialog : Form private void InitializeComponent() { - // Form properties Text = "Quick Send โ€” Moltbot"; - Size = new Size(500, 220); - StartPosition = FormStartPosition.CenterScreen; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; + Size = new Size(520, 300); ShowInTaskbar = true; - TopMost = true; // Always on top when opened via hotkey + TopMost = true; Icon = IconHelper.GetLobsterIcon(); - // Label - var label = new Label - { - Text = "Send a message to Moltbot:", - Location = new Point(12, 12), - Size = new Size(460, 20), - Font = new Font("Segoe UI", 9.5F, FontStyle.Regular) - }; + // Header label + var label = CreateModernLabel("Send a message to Moltbot:"); + label.Location = new Point(20, 20); + label.Font = new Font("Segoe UI", 11F, FontStyle.Bold); + label.ForeColor = AccentColor; // Message text box - _messageTextBox = new TextBox - { - Location = new Point(12, 36), - Size = new Size(460, 90), - Multiline = true, - ScrollBars = ScrollBars.Vertical, - Font = new Font("Segoe UI", 10F, FontStyle.Regular), - AcceptsReturn = false // Enter sends, Shift+Enter for newline - }; + _messageTextBox = CreateModernTextBox(); + _messageTextBox.Location = new Point(20, 52); + _messageTextBox.Size = new Size(464, 110); + _messageTextBox.Multiline = true; + _messageTextBox.ScrollBars = ScrollBars.Vertical; + _messageTextBox.AcceptsReturn = false; + _messageTextBox.Font = new Font("Segoe UI", 10.5f); - // Hint label - _hintLabel = new Label - { - Text = "Enter to send ยท Esc to cancel ยท Shift+Enter for new line", - Location = new Point(12, 132), - Size = new Size(300, 18), - Font = new Font("Segoe UI", 8F, FontStyle.Regular), - ForeColor = Color.Gray - }; - - // Send button - _sendButton = new Button - { - Text = "&Send", - Location = new Point(316, 148), - Size = new Size(75, 28), - UseVisualStyleBackColor = true, - Font = new Font("Segoe UI", 9F, FontStyle.Regular) - }; + // Buttons row (below text box) + _sendButton = CreateModernButton("Send", isPrimary: true); + _sendButton.Location = new Point(394, 172); + _sendButton.Size = new Size(90, 32); _sendButton.Click += OnSendClick; - // Cancel button - _cancelButton = new Button - { - Text = "&Cancel", - Location = new Point(397, 148), - Size = new Size(75, 28), - UseVisualStyleBackColor = true, - Font = new Font("Segoe UI", 9F, FontStyle.Regular) - }; + _cancelButton = CreateModernButton("Cancel"); + _cancelButton.Location = new Point(296, 172); + _cancelButton.Size = new Size(90, 32); _cancelButton.Click += OnCancelClick; - // Set dialog buttons + // Hint label (below buttons with more space) + _hintLabel = CreateModernLabel("Enter to send ยท Esc to cancel ยท Shift+Enter for new line", isSubtle: true); + _hintLabel.Location = new Point(20, 220); + _hintLabel.Font = new Font("Segoe UI", 8.5F); + AcceptButton = _sendButton; CancelButton = _cancelButton; - // Add controls - Controls.Add(label); - Controls.Add(_messageTextBox); - Controls.Add(_hintLabel); - Controls.Add(_sendButton); - Controls.Add(_cancelButton); + Controls.AddRange(new Control[] { label, _messageTextBox, _sendButton, _cancelButton, _hintLabel }); - // Focus the text box on show Shown += (_, _) => { _messageTextBox.Focus(); - Activate(); // Ensure window is focused when opened via hotkey + Activate(); }; } @@ -109,7 +76,6 @@ public partial class QuickSendDialog : Form _messageTextBox.Focus(); return; } - DialogResult = DialogResult.OK; Close(); } @@ -122,13 +88,11 @@ public partial class QuickSendDialog : Form protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { - // Ctrl+Enter or Enter (without Shift) as send if (keyData == (Keys.Control | Keys.Enter) || keyData == Keys.Enter) { OnSendClick(null, EventArgs.Empty); return true; } - return base.ProcessCmdKey(ref msg, keyData); } } diff --git a/src/Moltbot.Tray/SettingsDialog.cs b/src/Moltbot.Tray/SettingsDialog.cs index 9d194ed..0a69393 100644 --- a/src/Moltbot.Tray/SettingsDialog.cs +++ b/src/Moltbot.Tray/SettingsDialog.cs @@ -1,3 +1,4 @@ +using Microsoft.Toolkit.Uwp.Notifications; using Moltbot.Shared; using System; using System.Drawing; @@ -5,7 +6,7 @@ using System.Windows.Forms; namespace MoltbotTray; -public partial class SettingsDialog : Form +public partial class SettingsDialog : ModernForm { private readonly SettingsManager _settings; @@ -16,6 +17,7 @@ public partial class SettingsDialog : Form private CheckBox _globalHotkeyCheckBox = null!; private ComboBox _notificationSoundComboBox = null!; private Button _testConnectionButton = null!; + private Button _testNotificationButton = null!; private Button _okButton = null!; private Button _cancelButton = null!; private Label _statusLabel = null!; @@ -42,181 +44,121 @@ public partial class SettingsDialog : Form { Text = "Settings โ€” Moltbot Tray"; Size = new Size(480, 560); - StartPosition = FormStartPosition.CenterScreen; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; ShowInTaskbar = false; AutoScroll = true; Icon = IconHelper.GetLobsterIcon(); - var y = 12; - var labelFont = new Font("Segoe UI", 9F); - var headerFont = new Font("Segoe UI", 9F, FontStyle.Bold); + var y = 16; // --- Connection Section --- - var connHeader = new Label - { - Text = "CONNECTION", - Location = new Point(12, y), - Size = new Size(200, 20), - Font = headerFont, - ForeColor = Color.FromArgb(0, 120, 215) - }; - y += 22; - - var gatewayUrlLabel = new Label - { - Text = "Gateway URL:", - Location = new Point(12, y), - Size = new Size(100, 20), - Font = labelFont - }; - y += 22; - - _gatewayUrlTextBox = new TextBox - { - Location = new Point(12, y), - Size = new Size(310, 23), - Font = labelFont - }; - - _testConnectionButton = new Button - { - Text = "Test", - Location = new Point(330, y - 1), - Size = new Size(65, 25), - Font = labelFont - }; - _testConnectionButton.Click += OnTestConnection; - y += 30; - - var tokenLabel = new Label - { - Text = "Token:", - Location = new Point(12, y), - Size = new Size(100, 20), - Font = labelFont - }; - y += 22; - - _tokenTextBox = new TextBox - { - Location = new Point(12, y), - Size = new Size(310, 23), - Font = labelFont, - UseSystemPasswordChar = true - }; - - _statusLabel = new Label - { - Text = "", - Location = new Point(330, y + 2), - Size = new Size(130, 20), - Font = new Font("Segoe UI", 8F), - ForeColor = Color.DarkGreen - }; - y += 35; - - // --- Startup Section --- - var startupHeader = new Label - { - Text = "STARTUP", - Location = new Point(12, y), - Size = new Size(200, 20), - Font = headerFont, - ForeColor = Color.FromArgb(0, 120, 215) - }; - y += 22; - - _autoStartCheckBox = new CheckBox - { - Text = "Start automatically with Windows", - Location = new Point(12, y), - Size = new Size(280, 22), - Font = labelFont - }; + var connHeader = CreateModernLabel("CONNECTION"); + connHeader.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + connHeader.ForeColor = AccentColor; + connHeader.Location = new Point(16, y); y += 26; - _globalHotkeyCheckBox = new CheckBox - { - Text = "Global hotkey (Ctrl+Alt+Shift+C โ†’ Quick Send)", - Location = new Point(12, y), - Size = new Size(340, 22), - Font = labelFont - }; - y += 35; + var gatewayUrlLabel = CreateModernLabel("Gateway URL:"); + gatewayUrlLabel.Location = new Point(16, y); + y += 24; + + _gatewayUrlTextBox = CreateModernTextBox(); + _gatewayUrlTextBox.Location = new Point(16, y); + _gatewayUrlTextBox.Size = new Size(310, 28); + + _testConnectionButton = CreateModernButton("Test"); + _testConnectionButton.Location = new Point(334, y - 2); + _testConnectionButton.Size = new Size(70, 30); + _testConnectionButton.Click += OnTestConnection; + y += 36; + + var tokenLabel = CreateModernLabel("Token:"); + tokenLabel.Location = new Point(16, y); + y += 24; + + _tokenTextBox = CreateModernTextBox(); + _tokenTextBox.Location = new Point(16, y); + _tokenTextBox.Size = new Size(310, 28); + _tokenTextBox.UseSystemPasswordChar = true; + + _statusLabel = CreateModernLabel("", isSubtle: true); + _statusLabel.Location = new Point(334, y + 4); + _statusLabel.Font = new Font("Segoe UI", 8.5F); + y += 44; + + // --- Startup Section --- + var startupHeader = CreateModernLabel("STARTUP"); + startupHeader.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + startupHeader.ForeColor = AccentColor; + startupHeader.Location = new Point(16, y); + y += 26; + + _autoStartCheckBox = CreateModernCheckBox("Start automatically with Windows"); + _autoStartCheckBox.Location = new Point(16, y); + y += 28; + + _globalHotkeyCheckBox = CreateModernCheckBox("Global hotkey (Ctrl+Alt+Shift+C โ†’ Quick Send)"); + _globalHotkeyCheckBox.Location = new Point(16, y); + y += 40; // --- Notifications Section --- - var notifyHeader = new Label - { - Text = "NOTIFICATIONS", - Location = new Point(12, y), - Size = new Size(200, 20), - Font = headerFont, - ForeColor = Color.FromArgb(0, 120, 215) - }; - y += 22; + var notifyHeader = CreateModernLabel("NOTIFICATIONS"); + notifyHeader.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + notifyHeader.ForeColor = AccentColor; + notifyHeader.Location = new Point(16, y); + y += 26; - _showNotificationsCheckBox = new CheckBox - { - Text = "Show desktop notifications", - Location = new Point(12, y), - Size = new Size(250, 22), - Font = labelFont - }; + _showNotificationsCheckBox = CreateModernCheckBox("Show desktop notifications"); + _showNotificationsCheckBox.Location = new Point(16, y); _showNotificationsCheckBox.CheckedChanged += (_, _) => { _notifyFilterPanel.Enabled = _showNotificationsCheckBox.Checked; }; - y += 26; + y += 28; - var soundLabel = new Label - { - Text = "Sound:", - Location = new Point(12, y), - Size = new Size(50, 20), - Font = labelFont - }; + var soundLabel = CreateModernLabel("Sound:"); + soundLabel.Location = new Point(16, y + 3); + soundLabel.AutoSize = true; _notificationSoundComboBox = new ComboBox { - Location = new Point(65, y - 2), - Size = new Size(140, 23), + Location = new Point(80, y), + Size = new Size(140, 28), DropDownStyle = ComboBoxStyle.DropDownList, - Font = labelFont + Font = new Font("Segoe UI", 9.5f), + BackColor = SurfaceColor, + ForeColor = ForegroundColor, + FlatStyle = FlatStyle.Flat }; _notificationSoundComboBox.Items.AddRange(new[] { "Default", "None", "Critical", "Information" }); - y += 30; + + _testNotificationButton = CreateModernButton("Test"); + _testNotificationButton.Location = new Point(230, y); + _testNotificationButton.Size = new Size(80, 28); + _testNotificationButton.Click += OnTestNotification; + y += 36; // Filter panel - var filterLabel = new Label - { - Text = "Show toasts for:", - Location = new Point(12, y), - Size = new Size(120, 20), - Font = labelFont, - ForeColor = Color.Gray - }; - y += 22; + var filterLabel = CreateModernLabel("Show toasts for:", isSubtle: true); + filterLabel.Location = new Point(16, y); + y += 24; _notifyFilterPanel = new Panel { - Location = new Point(12, y), + Location = new Point(16, y), Size = new Size(440, 72), - BorderStyle = BorderStyle.None + BorderStyle = BorderStyle.None, + BackColor = Color.Transparent }; // Two columns of filter checkboxes - var cbFont = new Font("Segoe UI", 8.5F); - _notifyHealthCb = MakeFilterCb("๐Ÿฉธ Health", 0, 0, cbFont); - _notifyUrgentCb = MakeFilterCb("๐Ÿšจ Urgent", 0, 24, cbFont); - _notifyReminderCb = MakeFilterCb("โฐ Reminders", 0, 48, cbFont); - _notifyEmailCb = MakeFilterCb("๐Ÿ“ง Email", 150, 0, cbFont); - _notifyCalendarCb = MakeFilterCb("๐Ÿ“… Calendar", 150, 24, cbFont); - _notifyBuildCb = MakeFilterCb("๐Ÿ”จ Build/CI", 150, 48, cbFont); - _notifyStockCb = MakeFilterCb("๐Ÿ“ฆ Stock", 300, 0, cbFont); - _notifyInfoCb = MakeFilterCb("๐Ÿค– General", 300, 24, cbFont); + _notifyHealthCb = MakeFilterCb("๐Ÿฉธ Health", 0, 0); + _notifyUrgentCb = MakeFilterCb("๐Ÿšจ Urgent", 0, 24); + _notifyReminderCb = MakeFilterCb("โฐ Reminders", 0, 48); + _notifyEmailCb = MakeFilterCb("๐Ÿ“ง Email", 150, 0); + _notifyCalendarCb = MakeFilterCb("๐Ÿ“… Calendar", 150, 24); + _notifyBuildCb = MakeFilterCb("๐Ÿ”จ Build/CI", 150, 48); + _notifyStockCb = MakeFilterCb("๐Ÿ“ฆ Stock", 300, 0); + _notifyInfoCb = MakeFilterCb("๐Ÿค– General", 300, 24); _notifyFilterPanel.Controls.AddRange(new Control[] { @@ -225,28 +167,19 @@ public partial class SettingsDialog : Form _notifyStockCb, _notifyInfoCb }); - y += 80; + y += 90; // --- Buttons --- - y += 10; - _okButton = new Button - { - Text = "&OK", - Location = new Point(300, y), - Size = new Size(75, 28), - Font = labelFont - }; - _okButton.Click += OnOkClick; - - _cancelButton = new Button - { - Text = "&Cancel", - Location = new Point(382, y), - Size = new Size(75, 28), - Font = labelFont - }; + _cancelButton = CreateModernButton("Cancel"); + _cancelButton.Location = new Point(Width - 116, y); + _cancelButton.Size = new Size(90, 34); _cancelButton.Click += OnCancelClick; + _okButton = CreateModernButton("Save", isPrimary: true); + _okButton.Location = new Point(Width - 214, y); + _okButton.Size = new Size(90, 34); + _okButton.Click += OnOkClick; + AcceptButton = _okButton; CancelButton = _cancelButton; @@ -256,22 +189,35 @@ public partial class SettingsDialog : Form connHeader, gatewayUrlLabel, _gatewayUrlTextBox, _testConnectionButton, tokenLabel, _tokenTextBox, _statusLabel, startupHeader, _autoStartCheckBox, _globalHotkeyCheckBox, - notifyHeader, _showNotificationsCheckBox, soundLabel, _notificationSoundComboBox, + notifyHeader, _showNotificationsCheckBox, soundLabel, _notificationSoundComboBox, _testNotificationButton, filterLabel, _notifyFilterPanel, _okButton, _cancelButton }); } - private static CheckBox MakeFilterCb(string text, int x, int y, Font font) + private void OnTestNotification(object? sender, EventArgs e) { - return new CheckBox + try { - Text = text, - Location = new Point(x, y), - Size = new Size(140, 22), - Font = font, - Checked = true - }; + new ToastContentBuilder() + .AddText("๐Ÿฆž Test Notification") + .AddText("This is what a Moltbot notification looks like!") + .Show(); + } + catch + { + MessageBox.Show("Notifications may not be available on this system.", "Test", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + + private CheckBox MakeFilterCb(string text, int x, int y) + { + var cb = CreateModernCheckBox(text); + cb.Location = new Point(x, y); + cb.Size = new Size(140, 22); + cb.Font = new Font("Segoe UI", 8.5F); + cb.Checked = true; + return cb; } private void LoadSettings() diff --git a/src/Moltbot.Tray/StatusDetailForm.cs b/src/Moltbot.Tray/StatusDetailForm.cs index 054c3af..87bcee6 100644 --- a/src/Moltbot.Tray/StatusDetailForm.cs +++ b/src/Moltbot.Tray/StatusDetailForm.cs @@ -9,7 +9,7 @@ namespace MoltbotTray; /// /// Shows detailed gateway status, sessions, channels, and usage in a rich view. /// -public class StatusDetailForm : Form +public class StatusDetailForm : ModernForm { private RichTextBox _textBox = null!; private Button _refreshButton = null!; @@ -45,44 +45,39 @@ public class StatusDetailForm : Form private void InitializeComponent() { Text = "Moltbot Status"; - Size = new Size(520, 500); - MinimumSize = new Size(400, 350); - StartPosition = FormStartPosition.CenterScreen; + Size = new Size(540, 520); + MinimumSize = new Size(420, 380); + FormBorderStyle = FormBorderStyle.Sizable; Icon = IconHelper.GetLobsterIcon(); _textBox = new RichTextBox { Dock = DockStyle.Fill, ReadOnly = true, - Font = new Font("Cascadia Code", 10F, FontStyle.Regular, GraphicsUnit.Point), - BackColor = Color.FromArgb(30, 30, 30), - ForeColor = Color.FromArgb(220, 220, 220), + Font = new Font("Cascadia Code", 10F), + BackColor = IsDarkMode ? Color.FromArgb(25, 25, 25) : Color.FromArgb(252, 252, 252), + ForeColor = ForegroundColor, BorderStyle = BorderStyle.None, - WordWrap = true + WordWrap = true, + Padding = new Padding(8) }; - var buttonPanel = new FlowLayoutPanel + var buttonPanel = new Panel { Dock = DockStyle.Bottom, - Height = 40, - FlowDirection = FlowDirection.RightToLeft, - Padding = new Padding(5) + Height = 56, + BackColor = SurfaceColor, + Padding = new Padding(16, 12, 16, 12) }; - _closeButton = new Button - { - Text = "&Close", - Size = new Size(75, 26), - Font = new Font("Segoe UI", 9F) - }; + _closeButton = CreateModernButton("Close"); + _closeButton.Size = new Size(90, 36); + _closeButton.Anchor = AnchorStyles.Right | AnchorStyles.Top; _closeButton.Click += (_, _) => Close(); - _refreshButton = new Button - { - Text = "&Refresh", - Size = new Size(75, 26), - Font = new Font("Segoe UI", 9F) - }; + _refreshButton = CreateModernButton("Refresh", isPrimary: true); + _refreshButton.Size = new Size(90, 36); + _refreshButton.Anchor = AnchorStyles.Right | AnchorStyles.Top; _refreshButton.Click += async (_, _) => { if (_client != null) @@ -94,8 +89,18 @@ public class StatusDetailForm : Form RefreshStatus(); }; - buttonPanel.Controls.Add(_closeButton); - buttonPanel.Controls.Add(_refreshButton); + // Use FlowLayoutPanel for proper button layout + var buttonFlow = new FlowLayoutPanel + { + Dock = DockStyle.Right, + FlowDirection = FlowDirection.RightToLeft, + AutoSize = true, + BackColor = Color.Transparent + }; + buttonFlow.Controls.Add(_closeButton); + buttonFlow.Controls.Add(_refreshButton); + + buttonPanel.Controls.Add(buttonFlow); Controls.Add(_textBox); Controls.Add(buttonPanel); @@ -106,7 +111,7 @@ public class StatusDetailForm : Form var sb = new StringBuilder(); // Header - sb.AppendLine("โšก MOLTBOT STATUS"); + sb.AppendLine("๐Ÿฆž MOLTBOT STATUS"); sb.AppendLine(new string('โ”€', 40)); sb.AppendLine(); @@ -154,7 +159,7 @@ public class StatusDetailForm : Form sb.AppendLine($" Uptime: {GetUptime()}"); sb.AppendLine(); - // Auto-start + // Settings sb.AppendLine("โš™๏ธ SETTINGS"); sb.AppendLine(new string('โ”€', 40)); sb.AppendLine($" Auto-start: {(_settings?.AutoStart == true ? "โœ…" : "โŒ")}"); @@ -181,3 +186,4 @@ public class StatusDetailForm : Form } } + diff --git a/src/Moltbot.Tray/TrayApplication.cs b/src/Moltbot.Tray/TrayApplication.cs index 0c0f7cd..4b0fca7 100644 --- a/src/Moltbot.Tray/TrayApplication.cs +++ b/src/Moltbot.Tray/TrayApplication.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -16,6 +17,7 @@ public class TrayApplication : ApplicationContext { private NotifyIcon? _notifyIcon; private ContextMenuStrip? _contextMenu; + private ModernTrayMenu? _modernMenu; private MoltbotGatewayClient? _gatewayClient; private SettingsManager? _settings; private System.Windows.Forms.Timer? _healthCheckTimer; @@ -40,6 +42,11 @@ public class TrayApplication : ApplicationContext private readonly List _channelItems = new(); private readonly List _sessionItems = new(); + // Channel and session data for modern menu + private ChannelHealth[] _lastChannels = Array.Empty(); + private SessionInfo[] _lastSessions = Array.Empty(); + private GatewayUsageInfo? _lastUsage; + private readonly string[] _startupArgs; // P/Invoke for proper icon cleanup @@ -51,14 +58,28 @@ public class TrayApplication : ApplicationContext _startupArgs = args ?? Array.Empty(); _syncContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext(); Logger.Info("Application starting"); - InitializeComponent(); - InitializeAsync(); + try + { + InitializeComponent(); + InitializeAsync(); + } + catch (Exception ex) + { + Logger.Error($"Failed to initialize: {ex}"); + throw; + } } private void InitializeComponent() { _settings = new SettingsManager(); + // First-run check: show welcome if no token configured + if (string.IsNullOrWhiteSpace(_settings.Token)) + { + ShowFirstRunWelcome(); + } + // Register toast activation handler ToastNotificationManagerCompat.OnActivated += OnToastActivated; @@ -113,14 +134,18 @@ public class TrayApplication : ApplicationContext _contextMenu.Items.Add("Open Log File", null, OnOpenLogFile); _contextMenu.Items.Add("Exit", null, OnExit); - // Tray icon + // Modern tray menu (Windows 11 style) + _modernMenu = new ModernTrayMenu(); + _modernMenu.MenuItemClicked += OnModernMenuItemClicked; + + // Tray icon - use modern menu on right-click _notifyIcon = new NotifyIcon { Icon = CreateStatusIcon(ConnectionStatus.Disconnected), - ContextMenuStrip = _contextMenu, Text = "Moltbot Tray โ€” Disconnected", Visible = true }; + _notifyIcon.MouseClick += OnTrayIconClick; _notifyIcon.DoubleClick += OnDoubleClick; // Health check timer (30s) @@ -131,12 +156,243 @@ public class TrayApplication : ApplicationContext _sessionPollTimer = new System.Windows.Forms.Timer { Interval = 60000, Enabled = true }; _sessionPollTimer.Tick += OnSessionPoll; - // Global hotkey: Ctrl+Shift+Space โ†’ Quick Send + // Global hotkey: Ctrl+Alt+Shift+C โ†’ Quick Send _globalHotkey = new GlobalHotkey(); _globalHotkey.HotkeyPressed += (_, _) => OnQuickSend(null, EventArgs.Empty); _globalHotkey.Register(); } + private async void OnTrayIconClick(object? sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Right || e.Button == MouseButtons.Left) + { + // Request fresh data before showing menu + if (_gatewayClient != null && _currentStatus == ConnectionStatus.Connected) + { + try + { + // Fire off requests - don't await, just let them update the cache + _ = _gatewayClient.CheckHealthAsync(); + _ = _gatewayClient.RequestSessionsAsync(); + _ = _gatewayClient.RequestUsageAsync(); + // Small delay to let responses arrive + await Task.Delay(150); + } + catch { /* ignore - show cached data */ } + } + + // Build and show modern menu + BuildModernMenu(); + _modernMenu?.ShowAtCursor(); + } + } + + private void BuildModernMenu() + { + if (_modernMenu == null) return; + + _modernMenu.ClearItems(); + Logger.Info("Building modern menu..."); + + // Brand Header - big lobster! + _modernMenu.AddBrandHeader("๐Ÿฆž", "Molty"); + + // Status - use simple bullets that we can color + var (statusIcon, statusText, statusColor) = _currentStatus switch + { + ConnectionStatus.Connected => ("โ—", "Connected", Color.FromArgb(46, 204, 113)), + ConnectionStatus.Connecting => ("โ—", "Connecting...", Color.FromArgb(241, 196, 15)), + ConnectionStatus.Error => ("โ—", "Error", Color.FromArgb(231, 76, 60)), + _ => ("โ—‹", "Disconnected", Color.Gray) + }; + _modernMenu.AddStatusItem("status", statusIcon, "Gateway", statusText, statusColor); + + // Activity (if active) + if (_currentActivity?.Kind != ActivityKind.Idle && !string.IsNullOrEmpty(_currentActivity?.DisplayText)) + { + _modernMenu.AddItem("activity", "โ–ถ", _currentActivity.DisplayText, enabled: false); + } + + // Usage (if available) + if (_lastUsage != null) + { + _modernMenu.AddItem("usage", "โ—†", _lastUsage.DisplayText, enabled: false); + } + + _modernMenu.AddSeparator(); + + // Sessions (if any) - show meaningful info, clickable to go to /sessions + if (_lastSessions.Length > 0) + { + _modernMenu.AddItem("sessions", "โ—ˆ", "Sessions", isHeader: true); // Clickable header! + foreach (var session in _lastSessions.Take(5)) + { + // Extract session type from key like "agent:main:cron:uuid" or "agent:main:subagent:uuid" + var parts = session.Key.Split(':'); + var sessionType = parts.Length >= 3 ? parts[2] : "session"; + var displayName = sessionType switch + { + "main" => "Main Agent", + "cron" => "Scheduled Task", + "subagent" => "Sub-Agent", + _ => sessionType.Length > 0 ? char.ToUpper(sessionType[0]) + sessionType[1..] : "Session" + }; + + // Add model if available + if (!string.IsNullOrEmpty(session.Model)) + displayName += $" ({session.Model})"; + else if (!string.IsNullOrEmpty(session.Channel)) + displayName += $" ยท {session.Channel}"; + + var icon = session.IsMain ? "โ˜…" : "ยท"; + _modernMenu.AddItem($"session:{session.Key}", icon, displayName, enabled: false); + } + if (_lastSessions.Length > 5) + _modernMenu.AddItem("", "", $"+{_lastSessions.Length - 5} more...", enabled: false); + _modernMenu.AddSeparator(); + } + + // Channels (if any) + if (_lastChannels.Length > 0) + { + _modernMenu.AddItem("", "โ—‰", "Channels", isHeader: true); + foreach (var ch in _lastChannels) + { + var rawStatus = ch.Status?.ToLowerInvariant() ?? ""; + + // Normalize status display + // READY = configured and verified (linked or probe ok), ready to receive messages + // IDLE = configured but not verified (needs setup) + // ON = actively running/processing + var (statusLabel, color) = rawStatus switch + { + "ok" or "connected" or "running" or "active" => ("ON", Color.FromArgb(46, 204, 113)), + "ready" => ("READY", Color.FromArgb(46, 204, 113)), + "stopped" or "idle" or "paused" => ("IDLE", Color.FromArgb(241, 196, 15)), + "configured" or "pending" => ("IDLE", Color.FromArgb(241, 196, 15)), + "error" or "disconnected" or "failed" => ("ERROR", Color.FromArgb(231, 76, 60)), + "not configured" or "unconfigured" => ("N/A", Color.Gray), + _ => ("OFF", Color.Gray) + }; + _modernMenu.AddStatusItem($"channel:{ch.Name}", "โ—‹", char.ToUpper(ch.Name[0]) + ch.Name[1..], statusLabel, color); + } + _modernMenu.AddSeparator(); + } + + // Actions - use simple shapes we can color + _modernMenu.AddItem("dashboard", "โ—", "Open Dashboard"); + _modernMenu.AddItem("webchat", "โ—‰", "Open Web Chat"); + _modernMenu.AddItem("quicksend", "โ–ท", "Quick Send..."); + _modernMenu.AddItem("cron", "โฑ", "Cron Jobs"); + _modernMenu.AddItem("history", "โ‰ก", "Notification History"); + _modernMenu.AddItem("servicehealth", "โ™ฅ", "Service Health..."); + + _modernMenu.AddSeparator(); + + // Settings + _modernMenu.AddItem("settings", "โš™", "Settings..."); + _modernMenu.AddItem("autostart", _settings?.AutoStart == true ? "โœ“" : "โ—‹", + _settings?.AutoStart == true ? "Auto-start: On" : "Auto-start: Off"); + _modernMenu.AddItem("logs", "โ–ค", "Open Log File"); + + _modernMenu.AddSeparator(); + _modernMenu.AddItem("exit", "โœ•", "Exit"); + } + + private void OnModernMenuItemClicked(object? sender, string id) + { + switch (id) + { + case "status": + OnShowStatusDetail(null, EventArgs.Empty); + break; + case "dashboard": + OnOpenDashboard(null, EventArgs.Empty); + break; + case "webchat": + OnOpenWebUI(null, EventArgs.Empty); + break; + case "quicksend": + OnQuickSend(null, EventArgs.Empty); + break; + case "history": + OnNotificationHistory(null, EventArgs.Empty); + break; + case "servicehealth": + OnShowStatusDetail(null, EventArgs.Empty); + break; + case "sessions": + OpenDashboardPath("/sessions"); + break; + case "cron": + OpenDashboardPath("/cron"); + break; + case "settings": + OnSettings(null, EventArgs.Empty); + break; + case "autostart": + OnToggleAutoStart(null, EventArgs.Empty); + break; + case "logs": + OnOpenLogFile(null, EventArgs.Empty); + break; + case "exit": + OnExit(null, EventArgs.Empty); + break; + default: + // Handle channel toggle: "channel:telegram" etc. + if (id.StartsWith("channel:")) + { + var channelName = id[8..]; // Remove "channel:" prefix + _ = ToggleChannelAsync(channelName); + } + break; + } + } + + private void OpenDashboardPath(string path) + { + var dashboardUrl = GetDashboardUrl().TrimEnd('/') + path; + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = dashboardUrl, + UseShellExecute = true + }); + } + catch (Exception ex) + { + Logger.Instance.Error($"Failed to open dashboard path {path}", ex); + } + } + + private async Task ToggleChannelAsync(string channelName) + { + if (_gatewayClient == null) return; + + // Find the channel to check its current status + var channel = _lastChannels.FirstOrDefault(c => c.Name.Equals(channelName, StringComparison.OrdinalIgnoreCase)); + if (channel == null) return; + + var isRunning = channel.Status.ToLowerInvariant() is "ok" or "connected" or "running"; + + if (isRunning) + { + Logger.Info($"Stopping channel: {channelName}"); + await _gatewayClient.StopChannelAsync(channelName); + } + else + { + Logger.Info($"Starting channel: {channelName}"); + await _gatewayClient.StartChannelAsync(channelName); + } + + // Request fresh health data after a short delay + await Task.Delay(500); + await _gatewayClient.CheckHealthAsync(); + } + private async void InitializeAsync() { try @@ -308,6 +564,9 @@ public class TrayApplication : ApplicationContext private void UpdateChannelHealth(ChannelHealth[] channels) { + // Store for modern menu + _lastChannels = channels; + // Remove old channel items foreach (var item in _channelItems) _contextMenu?.Items.Remove(item); @@ -341,6 +600,9 @@ public class TrayApplication : ApplicationContext private void UpdateSessions(SessionInfo[] sessions) { + // Store for modern menu + _lastSessions = sessions; + // Log session data for debugging Logger.Info($"UpdateSessions: {sessions.Length} sessions"); foreach (var s in sessions) @@ -383,6 +645,9 @@ public class TrayApplication : ApplicationContext private void UpdateUsage(GatewayUsageInfo usage) { + // Store for modern menu + _lastUsage = usage; + if (_usageItem != null) { _usageItem.Text = $"๐Ÿ“Š {usage.DisplayText}"; @@ -704,6 +969,24 @@ public class TrayApplication : ApplicationContext } } + private void ShowFirstRunWelcome() + { + var dashboardUrl = _settings!.GatewayUrl + .Replace("ws://", "http://") + .Replace("wss://", "https://"); + + using var welcome = new WelcomeDialog(dashboardUrl); + if (welcome.ShowDialog() == DialogResult.OK) + { + // User clicked "Open Settings" + using var settings = new SettingsDialog(_settings); + if (settings.ShowDialog() == DialogResult.OK) + { + _settings.Save(); + } + } + } + private void OnToggleAutoStart(object? sender, EventArgs e) { var menuItem = (ToolStripMenuItem)sender!; @@ -789,6 +1072,7 @@ public class TrayApplication : ApplicationContext _healthCheckTimer?.Dispose(); _sessionPollTimer?.Dispose(); _gatewayClient?.Dispose(); + _modernMenu?.Dispose(); _notifyIcon?.Dispose(); _contextMenu?.Dispose(); Logger.Shutdown(); @@ -802,5 +1086,3 @@ public class TrayApplication : ApplicationContext base.ExitThreadCore(); } } - - diff --git a/src/Moltbot.Tray/UpdateDialog.cs b/src/Moltbot.Tray/UpdateDialog.cs index 63902c3..bfd0bb0 100644 --- a/src/Moltbot.Tray/UpdateDialog.cs +++ b/src/Moltbot.Tray/UpdateDialog.cs @@ -11,93 +11,43 @@ public enum UpdateDialogResult Skip } -public class UpdateDialog : Form +public class UpdateDialog : ModernForm { public UpdateDialogResult Result { get; private set; } = UpdateDialogResult.RemindLater; public UpdateDialog(string version, string releaseNotes) { - Text = "Update Available - Moltbot Tray"; - Size = new Size(500, 400); - StartPosition = FormStartPosition.CenterScreen; - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - MinimizeBox = false; - Icon = SystemIcons.Information; + Text = "Update Available โ€” Moltbot Tray"; + Size = new Size(500, 420); + Icon = IconHelper.GetLobsterIcon(); - var titleLabel = new Label - { - Text = "๐Ÿฆž Update Available!", - Font = new Font(Font.FontFamily, 14, FontStyle.Bold), - Location = new Point(20, 20), - AutoSize = true - }; + var titleLabel = CreateModernLabel("๐Ÿฆž Update Available!"); + titleLabel.Font = new Font("Segoe UI", 14, FontStyle.Bold); + titleLabel.ForeColor = AccentColor; + titleLabel.Location = new Point(20, 20); Controls.Add(titleLabel); - var versionLabel = new Label - { - Text = $"Version {version} is ready to install", - Location = new Point(20, 55), - AutoSize = true - }; + var versionLabel = CreateModernLabel($"Version {version} is ready to install"); + versionLabel.Location = new Point(20, 55); Controls.Add(versionLabel); - var notesLabel = new Label - { - Text = "Release Notes:", - Font = new Font(Font.FontFamily, 9, FontStyle.Bold), - Location = new Point(20, 85), - AutoSize = true - }; + var notesLabel = CreateModernLabel("Release Notes:"); + notesLabel.Font = new Font("Segoe UI", 9.5f, FontStyle.Bold); + notesLabel.Location = new Point(20, 90); Controls.Add(notesLabel); - var notesBox = new TextBox - { - Text = string.IsNullOrWhiteSpace(releaseNotes) ? "No release notes available." : releaseNotes, - Multiline = true, - ReadOnly = true, - ScrollBars = ScrollBars.Vertical, - Location = new Point(20, 110), - Size = new Size(440, 180), - BackColor = SystemColors.Window - }; + var notesBox = CreateModernTextBox(); + notesBox.Text = string.IsNullOrWhiteSpace(releaseNotes) ? "No release notes available." : releaseNotes; + notesBox.Multiline = true; + notesBox.ReadOnly = true; + notesBox.ScrollBars = ScrollBars.Vertical; + notesBox.Location = new Point(20, 115); + notesBox.Size = new Size(444, 200); Controls.Add(notesBox); - var downloadButton = new Button - { - Text = "Download && Install", - Size = new Size(130, 35), - Location = new Point(20, 310), - Font = new Font(Font.FontFamily, 9, FontStyle.Bold) - }; - downloadButton.Click += (_, _) => - { - Result = UpdateDialogResult.Download; - DialogResult = DialogResult.OK; - Close(); - }; - Controls.Add(downloadButton); - - var remindButton = new Button - { - Text = "Remind Me Later", - Size = new Size(130, 35), - Location = new Point(170, 310) - }; - remindButton.Click += (_, _) => - { - Result = UpdateDialogResult.RemindLater; - DialogResult = DialogResult.Cancel; - Close(); - }; - Controls.Add(remindButton); - - var skipButton = new Button - { - Text = "Skip This Version", - Size = new Size(130, 35), - Location = new Point(320, 310) - }; + var skipButton = CreateModernButton("Skip Version"); + skipButton.Size = new Size(120, 36); + skipButton.Location = new Point(20, 330); skipButton.Click += (_, _) => { Result = UpdateDialogResult.Skip; @@ -106,7 +56,30 @@ public class UpdateDialog : Form }; Controls.Add(skipButton); + var remindButton = CreateModernButton("Remind Later"); + remindButton.Size = new Size(120, 36); + remindButton.Location = new Point(230, 330); + remindButton.Click += (_, _) => + { + Result = UpdateDialogResult.RemindLater; + DialogResult = DialogResult.Cancel; + Close(); + }; + Controls.Add(remindButton); + + var downloadButton = CreateModernButton("Download && Install", isPrimary: true); + downloadButton.Size = new Size(140, 36); + downloadButton.Location = new Point(324, 330); + downloadButton.Click += (_, _) => + { + Result = UpdateDialogResult.Download; + DialogResult = DialogResult.OK; + Close(); + }; + Controls.Add(downloadButton); + AcceptButton = downloadButton; CancelButton = remindButton; } } + diff --git a/src/Moltbot.Tray/WebChatForm.cs b/src/Moltbot.Tray/WebChatForm.cs index 3d438c2..7a2f577 100644 --- a/src/Moltbot.Tray/WebChatForm.cs +++ b/src/Moltbot.Tray/WebChatForm.cs @@ -5,25 +5,23 @@ using System.Drawing; using System.IO; using System.Threading.Tasks; using System.Windows.Forms; +using Microsoft.Win32; namespace MoltbotTray; /// -/// Embeds the Moltbot WebChat UI via WebView2, matching the macOS native chat panel. +/// Embeds the Moltbot WebChat UI via WebView2 with modern Windows 11 styling. /// -public class WebChatForm : Form +public class WebChatForm : ModernForm { private WebView2? _webView; private readonly string _gatewayUrl; private readonly string _token; - private ToolStrip? _toolbar; + private Panel? _toolbar; private bool _initialized; private static WebChatForm? _instance; - /// - /// Show or focus the singleton WebChat window. - /// public static void ShowOrFocus(string gatewayUrl, string token) { if (_instance != null && !_instance.IsDisposed) @@ -50,26 +48,28 @@ public class WebChatForm : Form Text = "Moltbot Chat"; Size = new Size(520, 750); MinimumSize = new Size(380, 450); - StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.Sizable; Icon = IconHelper.GetLobsterIcon(); - BackColor = Color.FromArgb(30, 30, 30); - // Toolbar - _toolbar = new ToolStrip + // Modern toolbar panel - generous height for emoji rendering + _toolbar = new Panel { - GripStyle = ToolStripGripStyle.Hidden, - RenderMode = ToolStripRenderMode.System, - BackColor = Color.FromArgb(45, 45, 45), - ForeColor = Color.White + Dock = DockStyle.Top, + Height = 50, + BackColor = SurfaceColor }; - var homeBtn = new ToolStripButton("๐Ÿ  Home") { ForeColor = Color.White }; + var btnY = 8; + var homeBtn = CreateToolbarButton("๐Ÿ ", "Home"); + homeBtn.Location = new Point(8, btnY); homeBtn.Click += (_, _) => NavigateToChat(); - var refreshBtn = new ToolStripButton("โ†ป Refresh") { ForeColor = Color.White }; + var refreshBtn = CreateToolbarButton("โ†ป", "Refresh"); + refreshBtn.Location = new Point(50, btnY); refreshBtn.Click += (_, _) => _webView?.Reload(); - var popoutBtn = new ToolStripButton("โ†— Browser") { ForeColor = Color.White }; + var popoutBtn = CreateToolbarButton("โ†—", "Open in Browser"); + popoutBtn.Location = new Point(92, btnY); popoutBtn.Click += (_, _) => { var url = _gatewayUrl.Replace("ws://", "http://").Replace("wss://", "https://"); @@ -77,43 +77,56 @@ public class WebChatForm : Form catch { } }; - var devToolsBtn = new ToolStripButton("๐Ÿ”ง DevTools") { ForeColor = Color.White }; + var devToolsBtn = CreateToolbarButton("๐Ÿ”ง", "DevTools"); + devToolsBtn.Location = new Point(134, btnY); devToolsBtn.Click += (_, _) => _webView?.CoreWebView2?.OpenDevToolsWindow(); - _toolbar.Items.Add(homeBtn); - _toolbar.Items.Add(refreshBtn); - _toolbar.Items.Add(popoutBtn); - _toolbar.Items.Add(new ToolStripSeparator()); - _toolbar.Items.Add(devToolsBtn); + _toolbar.Controls.AddRange(new Control[] { homeBtn, refreshBtn, popoutBtn, devToolsBtn }); // WebView2 fills remaining space _webView = new WebView2 { Dock = DockStyle.Fill, - DefaultBackgroundColor = Color.FromArgb(30, 30, 30) + DefaultBackgroundColor = IsDarkMode ? Color.FromArgb(25, 25, 25) : Color.FromArgb(250, 250, 250) }; - // Controls layout โ€” toolbar on top, webview fills rest Controls.Add(_webView); Controls.Add(_toolbar); - _toolbar.Dock = DockStyle.Top; + } + + private Button CreateToolbarButton(string icon, string tooltip) + { + var btn = new Button + { + Text = icon, + Size = new Size(38, 34), + FlatStyle = FlatStyle.Flat, + Font = new Font("Segoe UI Symbol", 12), + Cursor = Cursors.Hand, + BackColor = Color.Transparent, + ForeColor = ForegroundColor, + UseCompatibleTextRendering = true + }; + btn.FlatAppearance.BorderSize = 0; + btn.FlatAppearance.MouseOverBackColor = HoverColor; + + var toolTip = new ToolTip(); + toolTip.SetToolTip(btn, tooltip); + + return btn; } private async Task InitializeWebViewAsync() { try { - // Use a dedicated user data folder var userDataDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MoltbotTray", "WebView2"); - var env = await CoreWebView2Environment.CreateAsync( - userDataFolder: userDataDir); - + var env = await CoreWebView2Environment.CreateAsync(userDataFolder: userDataDir); await _webView!.EnsureCoreWebView2Async(env); - // Configure WebView2 var settings = _webView.CoreWebView2.Settings; settings.IsStatusBarEnabled = false; settings.AreDefaultContextMenusEnabled = true; @@ -121,7 +134,6 @@ public class WebChatForm : Form _initialized = true; Logger.Info("WebView2 initialized"); - NavigateToChat(); } catch (WebView2RuntimeNotFoundException) @@ -155,12 +167,10 @@ public class WebChatForm : Form { if (!_initialized || _webView?.CoreWebView2 == null) return; - // Convert ws:// to http:// for the web UI var httpUrl = _gatewayUrl .Replace("ws://", "http://") .Replace("wss://", "https://"); - // The gateway serves WebChat at the root with token auth var chatUrl = $"{httpUrl}?token={Uri.EscapeDataString(_token)}"; _webView.CoreWebView2.Navigate(chatUrl); Logger.Info($"Navigating to WebChat: {httpUrl}"); @@ -174,3 +184,4 @@ public class WebChatForm : Form } } + diff --git a/src/Moltbot.Tray/WelcomeDialog.cs b/src/Moltbot.Tray/WelcomeDialog.cs new file mode 100644 index 0000000..45ef1f5 --- /dev/null +++ b/src/Moltbot.Tray/WelcomeDialog.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Windows.Forms; + +namespace MoltbotTray; + +/// +/// First-run welcome dialog to help users get started with Moltbot. +/// +public class WelcomeDialog : ModernForm +{ + private readonly string _dashboardUrl; + + public WelcomeDialog(string dashboardUrl) + { + _dashboardUrl = dashboardUrl; + InitializeComponent(); + } + + private void InitializeComponent() + { + Text = "Welcome to Molty"; + Size = new Size(500, 380); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + StartPosition = FormStartPosition.CenterScreen; + Icon = IconHelper.GetLobsterIcon(); + + var y = 20; + + // Lobster header + var headerLabel = new Label + { + Text = "๐Ÿฆž", + Font = new Font("Segoe UI Emoji", 36), + Location = new Point(0, y), + Size = new Size(ClientSize.Width, 60), + TextAlign = ContentAlignment.MiddleCenter, + ForeColor = AccentColor + }; + y += 70; + + // Welcome text + var welcomeLabel = new Label + { + Text = "Welcome to Molty!", + Font = new Font("Segoe UI", 14, FontStyle.Bold), + Location = new Point(0, y), + Size = new Size(ClientSize.Width, 30), + TextAlign = ContentAlignment.MiddleCenter, + ForeColor = ForegroundColor, + BackColor = Color.Transparent + }; + y += 40; + + // Instructions + var instructionsLabel = CreateModernLabel( + "To get started, you'll need an API token from your\n" + + "Moltbot dashboard. Click below to learn how to get one,\n" + + "then paste your token in Settings."); + instructionsLabel.Font = new Font("Segoe UI", 9.5f); + instructionsLabel.Location = new Point(30, y); + instructionsLabel.Size = new Size(ClientSize.Width - 60, 60); + instructionsLabel.TextAlign = ContentAlignment.MiddleCenter; + y += 85; + + // Learn about tokens button + var learnBtn = CreateModernButton("Learn How to Get a Token", isPrimary: true); + learnBtn.Location = new Point((ClientSize.Width - 250) / 2, y); + learnBtn.Size = new Size(250, 40); + learnBtn.Click += (_, _) => + { + try + { + Process.Start(new ProcessStartInfo("https://docs.molt.bot/web/dashboard") { UseShellExecute = true }); + } + catch { } + }; + y += 55; + + // Open Settings button + var settingsBtn = CreateModernButton("Open Settings"); + settingsBtn.Location = new Point((ClientSize.Width - 160) / 2, y); + settingsBtn.Size = new Size(160, 36); + settingsBtn.Click += (_, _) => + { + DialogResult = DialogResult.OK; + Close(); + }; + + Controls.AddRange(new Control[] { headerLabel, welcomeLabel, instructionsLabel, learnBtn, settingsBtn }); + } +}