Modern Windows 11 UI overhaul with Mac parity
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build-extension (x64) (push) Has been cancelled
Build and Test / build (win-arm64) (push) Has been cancelled
Build and Test / build (win-x64) (push) Has been cancelled
Build and Test / build-extension (arm64) (push) Has been cancelled
Build and Test / release (push) Has been cancelled
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build-extension (x64) (push) Has been cancelled
Build and Test / build (win-arm64) (push) Has been cancelled
Build and Test / build (win-x64) (push) Has been cancelled
Build and Test / build-extension (arm64) (push) Has been cancelled
Build and Test / release (push) Has been cancelled
- New ModernTrayMenu: Windows 11-style flyout replacing ContextMenuStrip - Dark/light mode auto-detection - Lobster branding header with accent colors - Clickable channel toggles (start/stop Telegram/WhatsApp) - Sessions link to /sessions, Cron Jobs to /cron - Status badges with color coding (READY/IDLE/ON/OFF) - New ModernForm base class for all dialogs - Rounded corners via DWM APIs - Consistent theming across Settings, QuickSend, WebChat, etc. - Accent color support - New WelcomeDialog for first-run experience - Guides users to get API token - Links to docs.molt.bot documentation - Opens Settings after onboarding - Channel status parity: unified READY status for linked channels - Service Health menu item (replaces Run Health Check) - Test Notification button in Settings - Various DPI and spacing fixes - Updated README with screenshot and expanded feature list
This commit is contained in:
parent
df3e5508a4
commit
40a68ec100
46
README.md
46
README.md
@ -2,6 +2,8 @@
|
||||
|
||||
A Windows companion suite for [Moltbot](https://moltbot.com) - the AI-powered personal assistant.
|
||||
|
||||

|
||||
|
||||
## 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)
|
||||
|
||||
BIN
docs/molty1.png
Normal file
BIN
docs/molty1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@ -163,6 +163,54 @@ public class MoltbotGatewayClient : IDisposable
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Start a channel (telegram, whatsapp, etc).</summary>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stop a channel (telegram, whatsapp, etc).</summary>
|
||||
public async Task<bool> 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";
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
257
src/Moltbot.Tray/ModernForm.cs
Normal file
257
src/Moltbot.Tray/ModernForm.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Base form with Windows 11 modern styling - dark/light mode, rounded corners, Moltbot branding.
|
||||
/// Inherit from this for consistent look across all dialogs.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a styled button with Moltbot branding.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a styled text box.
|
||||
/// </summary>
|
||||
protected TextBox CreateModernTextBox()
|
||||
{
|
||||
return new TextBox
|
||||
{
|
||||
Font = new Font("Segoe UI", 10f),
|
||||
BackColor = SurfaceColor,
|
||||
ForeColor = ForegroundColor,
|
||||
BorderStyle = BorderStyle.FixedSingle
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a styled label.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a styled checkbox.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a styled group box.
|
||||
/// </summary>
|
||||
protected GroupBox CreateModernGroupBox(string text)
|
||||
{
|
||||
return new GroupBox
|
||||
{
|
||||
Text = text,
|
||||
Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
|
||||
ForeColor = AccentColor,
|
||||
BackColor = Color.Transparent
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a styled panel with border.
|
||||
/// </summary>
|
||||
protected Panel CreateModernPanel()
|
||||
{
|
||||
return new Panel
|
||||
{
|
||||
BackColor = SurfaceColor,
|
||||
BorderStyle = BorderStyle.None,
|
||||
Padding = new Padding(12)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a horizontal separator line.
|
||||
/// </summary>
|
||||
protected Panel CreateSeparator()
|
||||
{
|
||||
return new Panel
|
||||
{
|
||||
Height = 1,
|
||||
BackColor = BorderColor,
|
||||
Dock = DockStyle.Top,
|
||||
Margin = new Padding(0, 8, 0, 8)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a styled progress bar.
|
||||
/// </summary>
|
||||
protected ProgressBar CreateModernProgressBar()
|
||||
{
|
||||
return new ProgressBar
|
||||
{
|
||||
Style = ProgressBarStyle.Continuous,
|
||||
Height = 6,
|
||||
ForeColor = AccentColor
|
||||
};
|
||||
}
|
||||
}
|
||||
435
src/Moltbot.Tray/ModernTrayMenu.cs
Normal file
435
src/Moltbot.Tray/ModernTrayMenu.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Modern flyout menu with Windows 11 styling - dark/light mode, rounded corners, acrylic blur.
|
||||
/// Replaces the dated ContextMenuStrip with a custom-drawn popup.
|
||||
/// </summary>
|
||||
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<ModernMenuItem> _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<string>? 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; }
|
||||
}
|
||||
}
|
||||
@ -6,9 +6,9 @@ using System.Windows.Forms;
|
||||
namespace MoltbotTray;
|
||||
|
||||
/// <summary>
|
||||
/// Shows recent notification history in a simple list view.
|
||||
/// Shows recent notification history in a modern styled list view.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -9,7 +9,7 @@ namespace MoltbotTray;
|
||||
/// <summary>
|
||||
/// Shows detailed gateway status, sessions, channels, and usage in a rich view.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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<ToolStripItem> _channelItems = new();
|
||||
private readonly List<ToolStripItem> _sessionItems = new();
|
||||
|
||||
// Channel and session data for modern menu
|
||||
private ChannelHealth[] _lastChannels = Array.Empty<ChannelHealth>();
|
||||
private SessionInfo[] _lastSessions = Array.Empty<SessionInfo>();
|
||||
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<string>();
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,25 +5,23 @@ using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace MoltbotTray;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Show or focus the singleton WebChat window.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
95
src/Moltbot.Tray/WelcomeDialog.cs
Normal file
95
src/Moltbot.Tray/WelcomeDialog.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace MoltbotTray;
|
||||
|
||||
/// <summary>
|
||||
/// First-run welcome dialog to help users get started with Moltbot.
|
||||
/// </summary>
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user