diff --git a/src/OpenClaw.Shared/SettingsData.cs b/src/OpenClaw.Shared/SettingsData.cs index a8096c0c..f0685f5b 100644 --- a/src/OpenClaw.Shared/SettingsData.cs +++ b/src/OpenClaw.Shared/SettingsData.cs @@ -32,6 +32,8 @@ public class SettingsData public bool NodeCanvasEnabled { get; set; } = true; public bool NodeScreenEnabled { get; set; } = true; public bool NodeCameraEnabled { get; set; } = true; + public bool ScreenRecordingConsentGiven { get; set; } = false; + public bool CameraRecordingConsentGiven { get; set; } = false; public bool NodeLocationEnabled { get; set; } = true; public bool NodeBrowserProxyEnabled { get; set; } = true; public bool NodeSttEnabled { get; set; } = false; diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index fc4f49af..dfdf2748 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -760,6 +760,7 @@ private static string TruncateMenuText(string text, int maxLength = 96) => private void AddRecentActivity( string line, string category = "general", + string? icon = null, string? dashboardPath = null, string? details = null, string? sessionKey = null, @@ -768,6 +769,7 @@ private void AddRecentActivity( ActivityStreamService.Add( category: category, title: line, + icon: icon, details: details, dashboardPath: dashboardPath, sessionKey: sessionKey, @@ -1670,6 +1672,7 @@ private void InitializeNodeService() _nodeService.ChannelHealthUpdated += OnChannelHealthUpdated; _nodeService.InvokeCompleted += OnNodeInvokeCompleted; _nodeService.GatewaySelfUpdated += OnGatewaySelfUpdated; + _nodeService.RecordingStateChanged += OnRecordingStateChanged; if (canRunGateway) { @@ -1866,6 +1869,30 @@ private void OnNodeStatusChanged(object? sender, ConnectionStatus status) } } + private void OnRecordingStateChanged(object? sender, RecordingStateEventArgs args) + { + var source = args.Type == RecordingType.Screen ? "Screen" : "Camera"; + if (args.IsActive) + { + var title = args.Type == RecordingType.Screen + ? LocalizationHelper.GetString("Activity_ScreenRecordingStarted") + : LocalizationHelper.GetString("Activity_CameraRecordingStarted"); + var duration = args.DurationMs > 0 ? $" ({args.DurationMs / 1000.0:0.#}s)" : ""; + AddRecentActivity($"{title}{duration}", category: "node", + icon: "🔴", + details: string.Format(LocalizationHelper.GetString("Activity_RecordingRequestedByAgent"), source)); + } + else + { + var title = args.Type == RecordingType.Screen + ? LocalizationHelper.GetString("Activity_ScreenRecordingComplete") + : LocalizationHelper.GetString("Activity_CameraRecordingComplete"); + AddRecentActivity(title, category: "node", + icon: "✅", + details: string.Format(LocalizationHelper.GetString("Activity_RecordingSentToAgent"), source)); + } + } + private void OnPairingStatusChanged(object? sender, OpenClaw.Shared.PairingStatusEventArgs args) { Logger.Info($"Pairing status: {args.Status}"); diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs new file mode 100644 index 00000000..5eee5db4 --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs @@ -0,0 +1,195 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using OpenClaw.Shared.Capabilities; +using OpenClawTray.Helpers; +using OpenClawTray.Services; +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using WinUIEx; + +namespace OpenClawTray.Dialogs; + +/// +/// Privacy consent dialog shown before the first screen or camera recording. +/// Parameterized by recording type so each capability gets its own consent. +/// +public sealed class RecordingConsentDialog : WindowEx +{ + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + private static readonly IntPtr HWND_TOPMOST = new(-1); + private static readonly IntPtr HWND_NOTOPMOST = new(-2); + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOSIZE = 0x0001; + + private readonly TaskCompletionSource _tcs = new(); + private bool _consented; + + public RecordingConsentDialog(RecordingType type) + { + var isScreen = type == RecordingType.Screen; + var headingKey = isScreen ? "RecordingConsent_ScreenTitle" : "RecordingConsent_CameraTitle"; + var descriptionKey = isScreen ? "RecordingConsent_ScreenDescription" : "RecordingConsent_CameraDescription"; + var emoji = isScreen ? "🖥️" : "📷"; + + Title = LocalizationHelper.GetString("RecordingConsent_WindowTitle"); + this.SetWindowSize(460, 340); + this.CenterOnScreen(); + this.SetIcon("Assets\\openclaw.ico"); + + SystemBackdrop = new MicaBackdrop(); + ExtendsContentIntoTitleBar = true; + + // Custom title bar + var titleBar = new Grid + { + Height = 48, + Padding = new Thickness(16, 0, 140, 0) + }; + titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var titleIcon = new TextBlock + { + Text = "🦞", + FontSize = 16, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0) + }; + Grid.SetColumn(titleIcon, 0); + titleBar.Children.Add(titleIcon); + + var titleText = new TextBlock + { + Text = LocalizationHelper.GetString("RecordingConsent_WindowTitle"), + FontSize = 13, + VerticalAlignment = VerticalAlignment.Center, + Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"] + }; + Grid.SetColumn(titleText, 1); + titleBar.Children.Add(titleText); + + SetTitleBar(titleBar); + + // Main layout + var outerGrid = new Grid(); + outerGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(48) }); + outerGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + + Grid.SetRow(titleBar, 0); + outerGrid.Children.Add(titleBar); + + var root = new Grid + { + Padding = new Thickness(32, 16, 32, 32), + RowSpacing = 16 + }; + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + // Header + var header = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 12 + }; + header.Children.Add(new TextBlock { Text = emoji, FontSize = 36 }); + header.Children.Add(new TextBlock + { + Text = LocalizationHelper.GetString(headingKey), + Style = (Style)Application.Current.Resources["SubtitleTextBlockStyle"], + VerticalAlignment = VerticalAlignment.Center + }); + Grid.SetRow(header, 0); + root.Children.Add(header); + + // Content + var content = new StackPanel { Spacing = 12 }; + content.Children.Add(new TextBlock + { + Text = LocalizationHelper.GetString(descriptionKey), + TextWrapping = TextWrapping.Wrap + }); + content.Children.Add(new TextBlock + { + Text = LocalizationHelper.GetString("RecordingConsent_Privacy"), + TextWrapping = TextWrapping.Wrap, + Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"] + }); + Grid.SetRow(content, 1); + root.Children.Add(content); + + // Buttons + var buttonPanel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Spacing = 8 + }; + + var denyButton = new Button + { + Content = LocalizationHelper.GetString("RecordingConsent_Deny") + }; + denyButton.Click += (s, e) => + { + Logger.Info($"[RecordingConsent] User denied {type} recording consent"); + _consented = false; + Close(); + }; + buttonPanel.Children.Add(denyButton); + + var allowButton = new Button + { + Content = LocalizationHelper.GetString("RecordingConsent_Allow"), + Style = (Style)Application.Current.Resources["AccentButtonStyle"] + }; + allowButton.Click += (s, e) => + { + Logger.Info($"[RecordingConsent] User allowed {type} recording consent"); + _consented = true; + Close(); + }; + buttonPanel.Children.Add(allowButton); + + Grid.SetRow(buttonPanel, 2); + root.Children.Add(buttonPanel); + + Grid.SetRow(root, 1); + outerGrid.Children.Add(root); + + Content = outerGrid; + + Closed += (s, e) => _tcs.TrySetResult(_consented); + + Logger.Info($"[RecordingConsent] {type} recording consent dialog shown"); + } + + public new Task ShowAsync() + { + Activate(); + + // Force to foreground since this may be triggered from a background context + try + { + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + if (hwnd != IntPtr.Zero) + { + // Briefly set topmost to guarantee visibility, then remove topmost flag + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + SetForegroundWindow(hwnd); + } + } + catch { /* best-effort */ } + + return _tcs.Task; + } +} diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/RecordingCountdownWindow.cs b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingCountdownWindow.cs new file mode 100644 index 00000000..79917cdb --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingCountdownWindow.cs @@ -0,0 +1,133 @@ +using Microsoft.UI; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using WinUIEx; + +namespace OpenClawTray.Dialogs; + +/// +/// Compact chromeless countdown overlay (3-2-1) shown before recording starts. +/// Displays as a small floating dark pill with a white countdown number. +/// +public sealed class RecordingCountdownWindow : WindowEx +{ + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + private static readonly IntPtr HWND_TOPMOST = new(-1); + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOSIZE = 0x0001; + private const int GWL_STYLE = -16; + private const int GWL_EXSTYLE = -20; + private const int WS_POPUP = unchecked((int)0x80000000); + private const int WS_VISIBLE = 0x10000000; + private const int WS_EX_TOOLWINDOW = 0x00000080; + private const int WS_EX_NOACTIVATE = 0x08000000; + private const uint SWP_FRAMECHANGED = 0x0020; + + private readonly TaskCompletionSource _tcs = new(); + private readonly TextBlock _countdownText; + private readonly DispatcherQueueTimer _timer; + private int _remaining; + + public RecordingCountdownWindow(int seconds = 3) + { + _remaining = seconds; + + Title = ""; + this.SetWindowSize(120, 120); + this.CenterOnScreen(); + ExtendsContentIntoTitleBar = true; + IsMinimizable = false; + IsMaximizable = false; + IsResizable = false; + + _countdownText = new TextBlock + { + Text = _remaining.ToString(), + FontSize = 56, + FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Foreground = new SolidColorBrush(Colors.White), + // Nudge up slightly to compensate for font descender space + Padding = new Thickness(0, 0, 0, 6) + }; + + // Solid dark circle on a fully transparent window + var pill = new Border + { + Background = new SolidColorBrush(global::Windows.UI.Color.FromArgb(230, 30, 30, 30)), + CornerRadius = new CornerRadius(60), + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Child = _countdownText + }; + + Content = new Grid + { + Background = new SolidColorBrush(Colors.Transparent), + Children = { pill } + }; + + _timer = DispatcherQueue.CreateTimer(); + _timer.Interval = TimeSpan.FromSeconds(1); + _timer.Tick += OnTick; + } + + private void OnTick(DispatcherQueueTimer sender, object args) + { + _remaining--; + + if (_remaining <= 0) + { + _timer.Stop(); + Close(); + return; + } + + _countdownText.Text = _remaining.ToString(); + } + + public Task ShowCountdownAsync() + { + Closed += (s, e) => _tcs.TrySetResult(); + + // Transparent window background so only the dark circle is visible + SystemBackdrop = new TransparentTintBackdrop(); + + Activate(); + + // Strip window chrome and make topmost + try + { + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + if (hwnd != IntPtr.Zero) + { + SetWindowLong(hwnd, GWL_STYLE, WS_POPUP | WS_VISIBLE); + var exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE); + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); + } + } + catch { /* best-effort */ } + + _timer.Start(); + + return _tcs.Task; + } +} diff --git a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml index 0254552b..fe4de731 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml @@ -58,7 +58,12 @@ - + + + + diff --git a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs index 4cb8fe29..d5855bb3 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ActivityPage.xaml.cs @@ -96,6 +96,8 @@ private static ActivityViewModel MapToViewModel(ActivityStreamItem item) return new ActivityViewModel { Title = item.Title, + Icon = item.Icon, + IconVisibility = string.IsNullOrWhiteSpace(item.Icon) ? Visibility.Collapsed : Visibility.Visible, Category = item.Category, TimeAgo = GetTimeAgo(item.Timestamp), DetailText = detailText, @@ -135,6 +137,8 @@ private void OnClearAll(object sender, RoutedEventArgs e) private class ActivityViewModel { public string Title { get; set; } = ""; + public string Icon { get; set; } = ""; + public Visibility IconVisibility { get; set; } public string Category { get; set; } = ""; public string TimeAgo { get; set; } = ""; public string DetailText { get; set; } = ""; diff --git a/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml index 733d3ad9..018246f5 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml @@ -73,6 +73,25 @@ + + + + + + + + + + + + @@ -82,9 +101,9 @@ BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" Padding="24,16"> -