feat: video capture frontend — consent, notifications, activity stream & settings (#292)
* feat: add recording state tracking to NodeService Add IsScreenRecording/IsCameraRecording properties and RecordingStateChanged event to NodeService. Wrap OnScreenRecord and OnCameraClip handlers to set state and raise events before/after async recording calls. This enables downstream UI components (tray icon, toasts, activity log) to react to recording lifecycle changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add toast notifications for screen and camera recording Show toasts on recording start, completion, and failure for both screen recording and camera clips. Extract reusable ShowToast helper and add localized strings for all 5 locales (en-us, fr-fr, zh-cn, zh-tw, nl-nl). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: log recording events to activity stream Add recording start/complete events with emoji indicators (🔴/✅) to the activity stream. Render emoji in a separate TextBlock element to prevent color emoji clipping by the card's CornerRadius clip mask. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add recording consent dialog before first recording Show a standalone WindowEx consent dialog the first time an agent requests screen or camera recording. Consent is tracked separately per recording type (ScreenRecordingConsentGiven, CameraRecordingConsentGiven) so users can allow screen recording without granting camera access. The dialog uses extend-into-titlebar styling, Mica backdrop, and SetForegroundWindow to ensure visibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add privacy settings UI and polish consent dialog - Add Privacy section to Settings with screen/camera recording toggles - Settings toggles auto-refresh when consent changes externally - Fix consent dialog z-order with HWND_TOPMOST technique - Fix button width (MinWidth instead of fixed Width) - Add SettingsManager.Saved event for cross-component reactivity - Allow button uses AccentButtonStyle for consistency - Remove misleading 'only asked once' from privacy text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: serialize consent dialogs and settings saves to prevent races - Add SemaphoreSlim guard in EnsureRecordingConsentAsync so concurrent recording requests coalesce onto a single consent dialog per type - Add lock around SettingsManager.Save() to prevent concurrent file writes - Update privacy toggle text in all 5 locales to clarify that enabling skips future consent prompts (e.g. 'Allow screen recording without prompting') Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add 3-2-1 countdown overlay before recording starts Show a translucent topmost countdown window (3 → 2 → 1) before screen and camera recordings begin, similar to Windows Snipping Tool. Gives users clear visual indication that recording is about to start. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(video): harden recording consent persistence Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: settings dirty-state guard, consent dialog copy, and tests - Add dirty-state guard to SettingsPage: external consent saves no longer overwrite unsaved user edits on the Settings page - Update consent dialog description in all 5 locales to explicitly state that the choice persists until changed in Settings - Add 4 focused tests for settings save thread safety, Saved event, consent persistence, and consent revocation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Christine Yan <christineyan@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Scott Hanselman <scott@hanselman.com>
This commit is contained in:
parent
b0ba9affa2
commit
adcccc9b56
@ -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;
|
||||
|
||||
@ -760,6 +760,7 @@ public partial class App : Application
|
||||
private void AddRecentActivity(
|
||||
string line,
|
||||
string category = "general",
|
||||
string? icon = null,
|
||||
string? dashboardPath = null,
|
||||
string? details = null,
|
||||
string? sessionKey = null,
|
||||
@ -768,6 +769,7 @@ public partial class App : Application
|
||||
ActivityStreamService.Add(
|
||||
category: category,
|
||||
title: line,
|
||||
icon: icon,
|
||||
details: details,
|
||||
dashboardPath: dashboardPath,
|
||||
sessionKey: sessionKey,
|
||||
@ -1670,6 +1672,7 @@ public partial class App : Application
|
||||
_nodeService.ChannelHealthUpdated += OnChannelHealthUpdated;
|
||||
_nodeService.InvokeCompleted += OnNodeInvokeCompleted;
|
||||
_nodeService.GatewaySelfUpdated += OnGatewaySelfUpdated;
|
||||
_nodeService.RecordingStateChanged += OnRecordingStateChanged;
|
||||
|
||||
if (canRunGateway)
|
||||
{
|
||||
@ -1866,6 +1869,30 @@ public partial class App : Application
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
|
||||
195
src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs
Normal file
195
src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Privacy consent dialog shown before the first screen or camera recording.
|
||||
/// Parameterized by recording type so each capability gets its own consent.
|
||||
/// </summary>
|
||||
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<bool> _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<bool> 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;
|
||||
}
|
||||
}
|
||||
133
src/OpenClaw.Tray.WinUI/Dialogs/RecordingCountdownWindow.cs
Normal file
133
src/OpenClaw.Tray.WinUI/Dialogs/RecordingCountdownWindow.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Compact chromeless countdown overlay (3-2-1) shown before recording starts.
|
||||
/// Displays as a small floating dark pill with a white countdown number.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -58,7 +58,12 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Title}" FontWeight="SemiBold"/>
|
||||
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Title}" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Icon}" FontSize="11" Margin="5,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IconVisibility}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding TimeAgo}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
|
||||
|
||||
@ -96,6 +96,8 @@ public sealed partial class ActivityPage : Page
|
||||
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 @@ public sealed partial class ActivityPage : Page
|
||||
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; } = "";
|
||||
|
||||
@ -73,6 +73,25 @@
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
<!-- Privacy -->
|
||||
<Expander x:Name="PrivacyExpander" IsExpanded="True"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||
AutomationProperties.AutomationId="PrivacyExpander">
|
||||
<Expander.Header>
|
||||
<TextBlock x:Uid="SettingsPrivacyHeader" Text="Privacy" Style="{StaticResource BodyStrongTextBlockStyle}"/>
|
||||
</Expander.Header>
|
||||
<StackPanel Spacing="12" Padding="4">
|
||||
<TextBlock x:Uid="SettingsPrivacyDescription" Text="Pre-approve capabilities so agents can use them without a permission prompt each time. You'll still see a countdown before recording starts."
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
<ToggleSwitch x:Uid="ScreenRecordingToggle" x:Name="ScreenRecordingToggle" Header="Allow screen recording"
|
||||
AutomationProperties.AutomationId="SettingsPageScreenRecording"/>
|
||||
<ToggleSwitch x:Uid="CameraRecordingToggle" x:Name="CameraRecordingToggle" Header="Allow camera recording"
|
||||
AutomationProperties.AutomationId="SettingsPageCameraRecording"/>
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@ -82,9 +101,9 @@
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0" Padding="24,16">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
|
||||
<Button x:Uid="CancelButton" x:Name="CancelButton" Content="Cancel" Width="80" Click="OnCancel"
|
||||
<Button x:Uid="CancelButton" x:Name="CancelButton" Content="Cancel" MinWidth="80" Click="OnCancel"
|
||||
AutomationProperties.AutomationId="SettingsPageCancel"/>
|
||||
<Button x:Uid="SaveButton" x:Name="SaveButton" Content="Save" Width="80" Click="OnSave"
|
||||
<Button x:Uid="SaveButton" x:Name="SaveButton" Content="Save" MinWidth="80" Click="OnSave"
|
||||
Style="{ThemeResource AccentButtonStyle}"
|
||||
AutomationProperties.AutomationId="SettingsPageSave"/>
|
||||
</StackPanel>
|
||||
|
||||
@ -14,6 +14,8 @@ public sealed partial class SettingsPage : Page
|
||||
{
|
||||
private HubWindow? _hub;
|
||||
private bool _initialized;
|
||||
private bool _saving;
|
||||
private bool _isDirty;
|
||||
|
||||
|
||||
public SettingsPage()
|
||||
@ -27,8 +29,52 @@ public sealed partial class SettingsPage : Page
|
||||
if (!_initialized && hub.Settings != null)
|
||||
{
|
||||
LoadSettings(hub.Settings);
|
||||
hub.Settings.Saved += OnExternalSettingsChanged;
|
||||
RegisterDirtyHandlers();
|
||||
_initialized = true;
|
||||
}
|
||||
else if (_initialized && hub.Settings != null)
|
||||
{
|
||||
ScreenRecordingToggle.IsOn = hub.Settings.ScreenRecordingConsentGiven;
|
||||
CameraRecordingToggle.IsOn = hub.Settings.CameraRecordingConsentGiven;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterDirtyHandlers()
|
||||
{
|
||||
void MarkDirty(object s, RoutedEventArgs e) { if (_initialized) _isDirty = true; }
|
||||
|
||||
AutoStartToggle.Toggled += MarkDirty;
|
||||
GlobalHotkeyToggle.Toggled += MarkDirty;
|
||||
NotificationsToggle.Toggled += MarkDirty;
|
||||
ScreenRecordingToggle.Toggled += MarkDirty;
|
||||
CameraRecordingToggle.Toggled += MarkDirty;
|
||||
NotificationSoundComboBox.SelectionChanged += (s, e) => { if (_initialized) _isDirty = true; };
|
||||
NotifyHealthCb.Checked += MarkDirty; NotifyHealthCb.Unchecked += MarkDirty;
|
||||
NotifyUrgentCb.Checked += MarkDirty; NotifyUrgentCb.Unchecked += MarkDirty;
|
||||
NotifyReminderCb.Checked += MarkDirty; NotifyReminderCb.Unchecked += MarkDirty;
|
||||
NotifyEmailCb.Checked += MarkDirty; NotifyEmailCb.Unchecked += MarkDirty;
|
||||
NotifyCalendarCb.Checked += MarkDirty; NotifyCalendarCb.Unchecked += MarkDirty;
|
||||
NotifyBuildCb.Checked += MarkDirty; NotifyBuildCb.Unchecked += MarkDirty;
|
||||
NotifyStockCb.Checked += MarkDirty; NotifyStockCb.Unchecked += MarkDirty;
|
||||
NotifyInfoCb.Checked += MarkDirty; NotifyInfoCb.Unchecked += MarkDirty;
|
||||
}
|
||||
|
||||
private void OnExternalSettingsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_hub?.Settings == null || _saving || _isDirty) return;
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
ScreenRecordingToggle.IsOn = _hub.Settings.ScreenRecordingConsentGiven;
|
||||
CameraRecordingToggle.IsOn = _hub.Settings.CameraRecordingConsentGiven;
|
||||
|
||||
// Show that the change is already persisted
|
||||
SaveButton.Content = "✓ Saved";
|
||||
var timer = DispatcherQueue.CreateTimer();
|
||||
timer.Interval = TimeSpan.FromSeconds(2);
|
||||
timer.Tick += (t, a) => { SaveButton.Content = "Save"; timer.Stop(); };
|
||||
timer.Start();
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadSettings(SettingsManager settings)
|
||||
@ -57,6 +103,9 @@ public sealed partial class SettingsPage : Page
|
||||
NotifyBuildCb.IsChecked = settings.NotifyBuild;
|
||||
NotifyStockCb.IsChecked = settings.NotifyStock;
|
||||
NotifyInfoCb.IsChecked = settings.NotifyInfo;
|
||||
|
||||
ScreenRecordingToggle.IsOn = settings.ScreenRecordingConsentGiven;
|
||||
CameraRecordingToggle.IsOn = settings.CameraRecordingConsentGiven;
|
||||
}
|
||||
|
||||
private void OnSave(object sender, RoutedEventArgs e)
|
||||
@ -80,7 +129,13 @@ public sealed partial class SettingsPage : Page
|
||||
s.NotifyStock = NotifyStockCb.IsChecked ?? true;
|
||||
s.NotifyInfo = NotifyInfoCb.IsChecked ?? true;
|
||||
|
||||
s.ScreenRecordingConsentGiven = ScreenRecordingToggle.IsOn;
|
||||
s.CameraRecordingConsentGiven = CameraRecordingToggle.IsOn;
|
||||
|
||||
_saving = true;
|
||||
s.Save();
|
||||
_saving = false;
|
||||
_isDirty = false;
|
||||
AutoStartManager.SetAutoStart(s.AutoStart);
|
||||
_hub.RaiseSettingsSaved();
|
||||
|
||||
@ -98,6 +153,7 @@ public sealed partial class SettingsPage : Page
|
||||
_initialized = false;
|
||||
LoadSettings(_hub.Settings);
|
||||
_initialized = true;
|
||||
_isDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ public static class ActivityStreamService
|
||||
string category,
|
||||
string title,
|
||||
string? details = null,
|
||||
string? icon = null,
|
||||
string? dashboardPath = null,
|
||||
string? sessionKey = null,
|
||||
string? nodeId = null)
|
||||
@ -32,6 +33,7 @@ public static class ActivityStreamService
|
||||
Timestamp = DateTime.Now,
|
||||
Category = string.IsNullOrWhiteSpace(category) ? "general" : category,
|
||||
Title = title,
|
||||
Icon = icon ?? "",
|
||||
Details = details ?? "",
|
||||
DashboardPath = dashboardPath,
|
||||
SessionKey = sessionKey,
|
||||
@ -125,6 +127,7 @@ public class ActivityStreamItem
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
public string Category { get; set; } = "general";
|
||||
public string Title { get; set; } = "";
|
||||
public string Icon { get; set; } = "";
|
||||
public string Details { get; set; } = "";
|
||||
public string? DashboardPath { get; set; }
|
||||
public string? SessionKey { get; set; }
|
||||
|
||||
@ -25,6 +25,9 @@ public sealed class NodeService : IDisposable
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
private readonly Func<FrameworkElement?> _rootProvider;
|
||||
private readonly SettingsManager? _settings;
|
||||
private readonly SemaphoreSlim _consentLock = new(1, 1);
|
||||
private TaskCompletionSource<bool>? _screenConsentInFlight;
|
||||
private TaskCompletionSource<bool>? _cameraConsentInFlight;
|
||||
private WindowsNodeClient? _nodeClient;
|
||||
private CanvasWindow? _canvasWindow;
|
||||
// Invariant: _a2uiCanvasWindow is only read/written from the UI dispatcher
|
||||
@ -137,7 +140,12 @@ public sealed class NodeService : IDisposable
|
||||
public event EventHandler<ChannelHealth[]>? ChannelHealthUpdated;
|
||||
public event EventHandler<NodeInvokeCompletedEventArgs>? InvokeCompleted;
|
||||
public event EventHandler<GatewaySelfInfo>? GatewaySelfUpdated;
|
||||
public event EventHandler<RecordingStateEventArgs>? RecordingStateChanged;
|
||||
|
||||
public bool IsScreenRecording { get; private set; }
|
||||
public bool IsCameraRecording { get; private set; }
|
||||
public bool IsAnyRecording => IsScreenRecording || IsCameraRecording;
|
||||
|
||||
public bool IsConnected => _nodeClient?.IsConnected ?? false;
|
||||
public string? NodeId => _nodeClient?.NodeId;
|
||||
public bool IsPendingApproval => _nodeClient?.IsPendingApproval ?? false;
|
||||
@ -1210,27 +1218,39 @@ public sealed class NodeService : IDisposable
|
||||
if ((now - _lastScreenCaptureNotification).TotalSeconds > 10)
|
||||
{
|
||||
_lastScreenCaptureNotification = now;
|
||||
try
|
||||
{
|
||||
new ToastContentBuilder()
|
||||
.AddText(LocalizationHelper.GetString("Toast_ScreenCaptured"))
|
||||
.AddText(LocalizationHelper.GetString("Toast_ScreenCapturedDetail"))
|
||||
.Show();
|
||||
}
|
||||
catch { /* ignore notification errors */ }
|
||||
ShowToast("Toast_ScreenCaptured", "Toast_ScreenCapturedDetail");
|
||||
}
|
||||
|
||||
return await _screenCaptureService.CaptureAsync(args);
|
||||
}
|
||||
|
||||
private Task<ScreenRecordResult> OnScreenRecord(ScreenRecordArgs args)
|
||||
private async Task<ScreenRecordResult> OnScreenRecord(ScreenRecordArgs args)
|
||||
{
|
||||
if (_screenRecordingService == null)
|
||||
{
|
||||
throw new InvalidOperationException("Screen recording service not available");
|
||||
}
|
||||
|
||||
return _screenRecordingService.RecordAsync(args);
|
||||
await EnsureRecordingConsentAsync(RecordingType.Screen);
|
||||
await ShowRecordingCountdownAsync();
|
||||
|
||||
SetRecordingState(RecordingType.Screen, true, args.DurationMs);
|
||||
try
|
||||
{
|
||||
ShowToast("Toast_ScreenRecordingStarted", "Toast_ScreenRecordingStartedDetail");
|
||||
var result = await _screenRecordingService.RecordAsync(args);
|
||||
ShowToast("Toast_ScreenRecordingComplete", "Toast_ScreenRecordingCompleteDetail");
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) when (ex is not InvalidOperationException)
|
||||
{
|
||||
ShowToast("Toast_ScreenRecordingFailed", "Toast_ScreenRecordingFailedDetail");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetRecordingState(RecordingType.Screen, false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1281,10 +1301,17 @@ public sealed class NodeService : IDisposable
|
||||
{
|
||||
throw new InvalidOperationException("Camera capture service not available");
|
||||
}
|
||||
|
||||
|
||||
await EnsureRecordingConsentAsync(RecordingType.Camera);
|
||||
await ShowRecordingCountdownAsync();
|
||||
|
||||
SetRecordingState(RecordingType.Camera, true, args.DurationMs);
|
||||
try
|
||||
{
|
||||
return await _cameraCaptureService.ClipAsync(args);
|
||||
ShowToast("Toast_CameraRecordingStarted", "Toast_CameraRecordingStartedDetail");
|
||||
var result = await _cameraCaptureService.ClipAsync(args);
|
||||
ShowToast("Toast_CameraRecordingComplete", "Toast_CameraRecordingCompleteDetail");
|
||||
return result;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
@ -1301,6 +1328,10 @@ public sealed class NodeService : IDisposable
|
||||
"Camera access blocked. Enable camera access for desktop apps in Windows Privacy settings.",
|
||||
ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetRecordingState(RecordingType.Camera, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LocationResult> GetLocationAsync(LocationGetArgs args)
|
||||
@ -1436,6 +1467,185 @@ public sealed class NodeService : IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Recording State
|
||||
|
||||
private void SetRecordingState(RecordingType type, bool isActive, int durationMs = 0)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case RecordingType.Screen: IsScreenRecording = isActive; break;
|
||||
case RecordingType.Camera: IsCameraRecording = isActive; break;
|
||||
}
|
||||
|
||||
RecordingStateChanged?.Invoke(this, new RecordingStateEventArgs
|
||||
{
|
||||
Type = type,
|
||||
IsActive = isActive,
|
||||
DurationMs = durationMs
|
||||
});
|
||||
}
|
||||
|
||||
private async Task EnsureRecordingConsentAsync(RecordingType type)
|
||||
{
|
||||
if (HasRecordingConsent(type)) return;
|
||||
|
||||
Task<bool>? existingConsentPrompt = null;
|
||||
TaskCompletionSource<bool>? ownedConsentPrompt = null;
|
||||
|
||||
await _consentLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Re-check after acquiring lock: a prior caller may have resolved consent.
|
||||
if (HasRecordingConsent(type)) return;
|
||||
|
||||
var inFlight = GetConsentPrompt(type);
|
||||
if (inFlight != null)
|
||||
{
|
||||
existingConsentPrompt = inFlight.Task;
|
||||
}
|
||||
else
|
||||
{
|
||||
ownedConsentPrompt = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
SetConsentPrompt(type, ownedConsentPrompt);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_consentLock.Release();
|
||||
}
|
||||
|
||||
if (existingConsentPrompt != null)
|
||||
{
|
||||
if (!await existingConsentPrompt)
|
||||
throw new InvalidOperationException("Recording denied: user has not given consent");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var consented = await ShowRecordingConsentDialogAsync(type);
|
||||
ownedConsentPrompt!.TrySetResult(consented);
|
||||
|
||||
if (!consented)
|
||||
throw new InvalidOperationException("Recording denied: user has not given consent");
|
||||
}
|
||||
catch
|
||||
{
|
||||
ownedConsentPrompt!.TrySetResult(false);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _consentLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (ReferenceEquals(GetConsentPrompt(type), ownedConsentPrompt))
|
||||
SetConsentPrompt(type, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_consentLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasRecordingConsent(RecordingType type)
|
||||
{
|
||||
return type == RecordingType.Screen
|
||||
? _settings?.ScreenRecordingConsentGiven == true
|
||||
: _settings?.CameraRecordingConsentGiven == true;
|
||||
}
|
||||
|
||||
private TaskCompletionSource<bool>? GetConsentPrompt(RecordingType type)
|
||||
{
|
||||
return type == RecordingType.Screen
|
||||
? _screenConsentInFlight
|
||||
: _cameraConsentInFlight;
|
||||
}
|
||||
|
||||
private void SetConsentPrompt(RecordingType type, TaskCompletionSource<bool>? prompt)
|
||||
{
|
||||
if (type == RecordingType.Screen)
|
||||
_screenConsentInFlight = prompt;
|
||||
else
|
||||
_cameraConsentInFlight = prompt;
|
||||
}
|
||||
|
||||
private Task<bool> ShowRecordingConsentDialogAsync(RecordingType type)
|
||||
{
|
||||
var dialogTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
if (!_dispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var dialog = new Dialogs.RecordingConsentDialog(type);
|
||||
var consented = await dialog.ShowAsync();
|
||||
|
||||
if (consented && _settings != null)
|
||||
{
|
||||
if (type == RecordingType.Screen)
|
||||
_settings.ScreenRecordingConsentGiven = true;
|
||||
else
|
||||
_settings.CameraRecordingConsentGiven = true;
|
||||
_settings.Save();
|
||||
}
|
||||
|
||||
dialogTcs.TrySetResult(consented);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"[RecordingConsent] Dialog error: {ex.Message}");
|
||||
dialogTcs.TrySetResult(false);
|
||||
}
|
||||
}))
|
||||
{
|
||||
throw new InvalidOperationException("Recording denied: unable to show consent prompt");
|
||||
}
|
||||
|
||||
return dialogTcs.Task;
|
||||
}
|
||||
|
||||
private async Task ShowRecordingCountdownAsync()
|
||||
{
|
||||
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
if (!_dispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var countdown = new Dialogs.RecordingCountdownWindow(3);
|
||||
await countdown.ShowCountdownAsync();
|
||||
tcs.TrySetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"[RecordingCountdown] Error: {ex.Message}");
|
||||
tcs.TrySetResult(); // Don't block recording if countdown fails
|
||||
}
|
||||
}))
|
||||
{
|
||||
// If we can't show the countdown, proceed anyway
|
||||
return;
|
||||
}
|
||||
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
private static void ShowToast(string titleKey, string detailKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
new ToastContentBuilder()
|
||||
.AddText(LocalizationHelper.GetString(titleKey))
|
||||
.AddText(LocalizationHelper.GetString(detailKey))
|
||||
.Show();
|
||||
}
|
||||
catch { /* ignore notification errors */ }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
@ -1478,3 +1688,16 @@ public sealed class NodeService : IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum RecordingType
|
||||
{
|
||||
Screen,
|
||||
Camera
|
||||
}
|
||||
|
||||
public sealed class RecordingStateEventArgs : EventArgs
|
||||
{
|
||||
public RecordingType Type { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
public int DurationMs { get; init; }
|
||||
}
|
||||
|
||||
@ -26,6 +26,11 @@ public class SettingsManager
|
||||
public static string SettingsDirectoryPath => DefaultSettingsDirectory;
|
||||
public static string SettingsPath => DefaultSettingsFilePath;
|
||||
|
||||
/// <summary>Raised after settings are persisted to disk.</summary>
|
||||
public event EventHandler? Saved;
|
||||
|
||||
private readonly object _saveLock = new();
|
||||
|
||||
// Connection
|
||||
public string GatewayUrl { get; set; } = "ws://localhost:18789";
|
||||
public string Token { get; set; } = "";
|
||||
@ -64,6 +69,8 @@ public class SettingsManager
|
||||
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;
|
||||
@ -157,6 +164,8 @@ public class SettingsManager
|
||||
NodeCanvasEnabled = loaded.NodeCanvasEnabled;
|
||||
NodeScreenEnabled = loaded.NodeScreenEnabled;
|
||||
NodeCameraEnabled = loaded.NodeCameraEnabled;
|
||||
ScreenRecordingConsentGiven = loaded.ScreenRecordingConsentGiven;
|
||||
CameraRecordingConsentGiven = loaded.CameraRecordingConsentGiven;
|
||||
NodeLocationEnabled = loaded.NodeLocationEnabled;
|
||||
NodeBrowserProxyEnabled = loaded.NodeBrowserProxyEnabled;
|
||||
NodeSttEnabled = loaded.NodeSttEnabled;
|
||||
@ -205,76 +214,82 @@ public class SettingsManager
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
lock (_saveLock)
|
||||
{
|
||||
Directory.CreateDirectory(_settingsDirectory);
|
||||
// Lock the tray data dir to current user + SYSTEM + Administrators —
|
||||
// it co-locates the MCP bearer token, settings.json (which embeds
|
||||
// gateway/bootstrap credentials), and diagnostics jsonl. Other apps
|
||||
// running as the same user could otherwise read these freely.
|
||||
OpenClaw.Shared.Mcp.McpAuthToken.TryRestrictDataDirectoryAcl(_settingsDirectory);
|
||||
|
||||
var data = new SettingsData
|
||||
try
|
||||
{
|
||||
GatewayUrl = GatewayUrl,
|
||||
Token = Token,
|
||||
BootstrapToken = string.IsNullOrWhiteSpace(BootstrapToken) ? null : BootstrapToken,
|
||||
UseSshTunnel = UseSshTunnel,
|
||||
SshTunnelUser = SshTunnelUser,
|
||||
SshTunnelHost = SshTunnelHost,
|
||||
SshTunnelRemotePort = SshTunnelRemotePort,
|
||||
SshTunnelLocalPort = SshTunnelLocalPort,
|
||||
AutoStart = AutoStart,
|
||||
GlobalHotkeyEnabled = GlobalHotkeyEnabled,
|
||||
ShowNotifications = ShowNotifications,
|
||||
NotificationSound = NotificationSound,
|
||||
NotifyHealth = NotifyHealth,
|
||||
NotifyUrgent = NotifyUrgent,
|
||||
NotifyReminder = NotifyReminder,
|
||||
NotifyEmail = NotifyEmail,
|
||||
NotifyCalendar = NotifyCalendar,
|
||||
NotifyBuild = NotifyBuild,
|
||||
NotifyStock = NotifyStock,
|
||||
NotifyInfo = NotifyInfo,
|
||||
EnableNodeMode = EnableNodeMode,
|
||||
NodeCanvasEnabled = NodeCanvasEnabled,
|
||||
NodeScreenEnabled = NodeScreenEnabled,
|
||||
NodeCameraEnabled = NodeCameraEnabled,
|
||||
NodeLocationEnabled = NodeLocationEnabled,
|
||||
NodeBrowserProxyEnabled = NodeBrowserProxyEnabled,
|
||||
NodeSttEnabled = NodeSttEnabled,
|
||||
SttLanguage = SttLanguage,
|
||||
SttModelName = SttModelName,
|
||||
SttSilenceTimeout = SttSilenceTimeout,
|
||||
VoiceTtsEnabled = VoiceTtsEnabled,
|
||||
VoiceAudioFeedback = VoiceAudioFeedback,
|
||||
NodeTtsEnabled = NodeTtsEnabled,
|
||||
TtsProvider = TtsProvider,
|
||||
TtsElevenLabsApiKey = ProtectSettingSecret(TtsElevenLabsApiKey),
|
||||
TtsElevenLabsModel = string.IsNullOrWhiteSpace(TtsElevenLabsModel) ? null : TtsElevenLabsModel,
|
||||
TtsElevenLabsVoiceId = string.IsNullOrWhiteSpace(TtsElevenLabsVoiceId) ? null : TtsElevenLabsVoiceId,
|
||||
TtsWindowsVoiceId = string.IsNullOrWhiteSpace(TtsWindowsVoiceId) ? null : TtsWindowsVoiceId,
|
||||
HubNavPaneOpen = HubNavPaneOpen,
|
||||
TtsPiperVoiceId = TtsPiperVoiceId,
|
||||
EnableMcpServer = EnableMcpServer,
|
||||
A2UIImageHosts = A2UIImageHosts.Count == 0 ? null : new List<string>(A2UIImageHosts),
|
||||
// McpOnlyMode is legacy — never written; remains null in serialized output.
|
||||
HasSeenActivityStreamTip = HasSeenActivityStreamTip,
|
||||
SkippedUpdateTag = string.IsNullOrWhiteSpace(SkippedUpdateTag) ? null : SkippedUpdateTag,
|
||||
PreferredGatewayId = string.IsNullOrWhiteSpace(PreferredGatewayId) ? null : PreferredGatewayId,
|
||||
NotifyChatResponses = NotifyChatResponses,
|
||||
PreferStructuredCategories = PreferStructuredCategories,
|
||||
UserRules = UserRules
|
||||
};
|
||||
Directory.CreateDirectory(_settingsDirectory);
|
||||
// Lock the tray data dir to current user + SYSTEM + Administrators —
|
||||
// it co-locates the MCP bearer token, settings.json (which embeds
|
||||
// gateway/bootstrap credentials), and diagnostics jsonl. Other apps
|
||||
// running as the same user could otherwise read these freely.
|
||||
OpenClaw.Shared.Mcp.McpAuthToken.TryRestrictDataDirectoryAcl(_settingsDirectory);
|
||||
|
||||
var json = data.ToJson();
|
||||
File.WriteAllText(_settingsFilePath, json);
|
||||
|
||||
Logger.Info("Settings saved");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to save settings: {ex.Message}");
|
||||
var data = new SettingsData
|
||||
{
|
||||
GatewayUrl = GatewayUrl,
|
||||
Token = Token,
|
||||
BootstrapToken = string.IsNullOrWhiteSpace(BootstrapToken) ? null : BootstrapToken,
|
||||
UseSshTunnel = UseSshTunnel,
|
||||
SshTunnelUser = SshTunnelUser,
|
||||
SshTunnelHost = SshTunnelHost,
|
||||
SshTunnelRemotePort = SshTunnelRemotePort,
|
||||
SshTunnelLocalPort = SshTunnelLocalPort,
|
||||
AutoStart = AutoStart,
|
||||
GlobalHotkeyEnabled = GlobalHotkeyEnabled,
|
||||
ShowNotifications = ShowNotifications,
|
||||
NotificationSound = NotificationSound,
|
||||
NotifyHealth = NotifyHealth,
|
||||
NotifyUrgent = NotifyUrgent,
|
||||
NotifyReminder = NotifyReminder,
|
||||
NotifyEmail = NotifyEmail,
|
||||
NotifyCalendar = NotifyCalendar,
|
||||
NotifyBuild = NotifyBuild,
|
||||
NotifyStock = NotifyStock,
|
||||
NotifyInfo = NotifyInfo,
|
||||
EnableNodeMode = EnableNodeMode,
|
||||
NodeCanvasEnabled = NodeCanvasEnabled,
|
||||
NodeScreenEnabled = NodeScreenEnabled,
|
||||
NodeCameraEnabled = NodeCameraEnabled,
|
||||
ScreenRecordingConsentGiven = ScreenRecordingConsentGiven,
|
||||
CameraRecordingConsentGiven = CameraRecordingConsentGiven,
|
||||
NodeLocationEnabled = NodeLocationEnabled,
|
||||
NodeBrowserProxyEnabled = NodeBrowserProxyEnabled,
|
||||
NodeSttEnabled = NodeSttEnabled,
|
||||
SttLanguage = SttLanguage,
|
||||
SttModelName = SttModelName,
|
||||
SttSilenceTimeout = SttSilenceTimeout,
|
||||
VoiceTtsEnabled = VoiceTtsEnabled,
|
||||
VoiceAudioFeedback = VoiceAudioFeedback,
|
||||
NodeTtsEnabled = NodeTtsEnabled,
|
||||
TtsProvider = TtsProvider,
|
||||
TtsElevenLabsApiKey = ProtectSettingSecret(TtsElevenLabsApiKey),
|
||||
TtsElevenLabsModel = string.IsNullOrWhiteSpace(TtsElevenLabsModel) ? null : TtsElevenLabsModel,
|
||||
TtsElevenLabsVoiceId = string.IsNullOrWhiteSpace(TtsElevenLabsVoiceId) ? null : TtsElevenLabsVoiceId,
|
||||
TtsWindowsVoiceId = string.IsNullOrWhiteSpace(TtsWindowsVoiceId) ? null : TtsWindowsVoiceId,
|
||||
HubNavPaneOpen = HubNavPaneOpen,
|
||||
TtsPiperVoiceId = TtsPiperVoiceId,
|
||||
EnableMcpServer = EnableMcpServer,
|
||||
A2UIImageHosts = A2UIImageHosts.Count == 0 ? null : new List<string>(A2UIImageHosts),
|
||||
// McpOnlyMode is legacy — never written; remains null in serialized output.
|
||||
HasSeenActivityStreamTip = HasSeenActivityStreamTip,
|
||||
SkippedUpdateTag = string.IsNullOrWhiteSpace(SkippedUpdateTag) ? null : SkippedUpdateTag,
|
||||
PreferredGatewayId = string.IsNullOrWhiteSpace(PreferredGatewayId) ? null : PreferredGatewayId,
|
||||
NotifyChatResponses = NotifyChatResponses,
|
||||
PreferStructuredCategories = PreferStructuredCategories,
|
||||
UserRules = UserRules
|
||||
};
|
||||
|
||||
var json = data.ToJson();
|
||||
File.WriteAllText(_settingsFilePath, json);
|
||||
|
||||
Logger.Info("Settings saved");
|
||||
Saved?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to save settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -688,6 +688,116 @@ Use one of these options:
|
||||
<data name="Toast_CameraBlockedDetail" xml:space="preserve">
|
||||
<value>Enable camera access in Windows Privacy settings for OpenClaw Tray</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Recording ==================== -->
|
||||
|
||||
<data name="Toast_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 Screen Recording Started</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw agent is recording your screen</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>✅ Screen Recording Complete</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>Screen recording has been sent to the agent</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailed" xml:space="preserve">
|
||||
<value>❌ Screen Recording Failed</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailedDetail" xml:space="preserve">
|
||||
<value>An error occurred while recording the screen</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 Camera Recording Started</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw agent is recording from your camera</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>✅ Camera Recording Complete</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>Camera clip has been sent to the agent</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Recording Consent Dialog ==================== -->
|
||||
|
||||
<data name="RecordingConsent_WindowTitle" xml:space="preserve">
|
||||
<value>OpenClaw · Permission Request</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenTitle" xml:space="preserve">
|
||||
<value>Allow screen recording?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraTitle" xml:space="preserve">
|
||||
<value>Allow camera recording?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenDescription" xml:space="preserve">
|
||||
<value>An agent is requesting to record your screen. This will capture video from your display and send it to the agent. Your choice will be remembered for future requests until you change it in Settings.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraDescription" xml:space="preserve">
|
||||
<value>An agent is requesting to record from your camera. This will capture video from your webcam and send it to the agent. Your choice will be remembered for future requests until you change it in Settings.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Privacy" xml:space="preserve">
|
||||
<value>You can change this later in Settings.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Allow" xml:space="preserve">
|
||||
<value>Allow recording</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Deny" xml:space="preserve">
|
||||
<value>Deny</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Settings: Privacy ==================== -->
|
||||
|
||||
<data name="Settings_PrivacyHeader" xml:space="preserve">
|
||||
<value>Privacy</value>
|
||||
</data>
|
||||
<data name="Settings_PrivacyDescription" xml:space="preserve">
|
||||
<value>Control which capabilities agents can use on this device.</value>
|
||||
</data>
|
||||
<data name="Settings_AllowScreenRecording" xml:space="preserve">
|
||||
<value>Allow screen recording</value>
|
||||
</data>
|
||||
<data name="Settings_AllowCameraRecording" xml:space="preserve">
|
||||
<value>Allow camera recording</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyHeader.Text" xml:space="preserve">
|
||||
<value>Privacy</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyDescription.Text" xml:space="preserve">
|
||||
<value>Pre-approve capabilities so agents can use them without a permission prompt each time. You'll still see a countdown before recording starts.</value>
|
||||
</data>
|
||||
<data name="ScreenRecordingToggle.Header" xml:space="preserve">
|
||||
<value>Allow screen recording</value>
|
||||
</data>
|
||||
<data name="CameraRecordingToggle.Header" xml:space="preserve">
|
||||
<value>Allow camera recording</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Activity Stream: Recording ==================== -->
|
||||
|
||||
<data name="Activity_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>Screen recording started</value>
|
||||
</data>
|
||||
<data name="Activity_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>Screen recording complete</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>Camera recording started</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>Camera recording complete</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingRequestedByAgent" xml:space="preserve">
|
||||
<value>{0} recording requested by agent</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingSentToAgent" xml:space="preserve">
|
||||
<value>{0} recording sent to agent</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Activity Stream Tip ==================== -->
|
||||
<data name="Toast_ActivityStreamTip" xml:space="preserve">
|
||||
<value>⚡ New: Activity Stream</value>
|
||||
</data>
|
||||
@ -2572,4 +2682,4 @@ On your gateway host (Mac/Linux), run:
|
||||
<data name="VoiceSettingsPage_PreviewVoiceButtonContent" xml:space="preserve">
|
||||
<value>▶ Preview Voice</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@ -688,6 +688,116 @@ Utilisez l'une de ces options :
|
||||
<data name="Toast_CameraBlockedDetail" xml:space="preserve">
|
||||
<value>Activez l'accès à la caméra dans les paramètres de confidentialité de Windows pour OpenClaw Tray</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Recording ==================== -->
|
||||
|
||||
<data name="Toast_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 Enregistrement d'écran démarré</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingStartedDetail" xml:space="preserve">
|
||||
<value>L'agent OpenClaw enregistre votre écran</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>✅ Enregistrement d'écran terminé</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>L'enregistrement d'écran a été envoyé à l'agent</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailed" xml:space="preserve">
|
||||
<value>❌ Échec de l'enregistrement d'écran</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailedDetail" xml:space="preserve">
|
||||
<value>Une erreur est survenue lors de l'enregistrement de l'écran</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 Enregistrement caméra démarré</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStartedDetail" xml:space="preserve">
|
||||
<value>L'agent OpenClaw enregistre depuis votre caméra</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>✅ Enregistrement caméra terminé</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>Le clip caméra a été envoyé à l'agent</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Recording Consent Dialog ==================== -->
|
||||
|
||||
<data name="RecordingConsent_WindowTitle" xml:space="preserve">
|
||||
<value>OpenClaw · Demande d'autorisation</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenTitle" xml:space="preserve">
|
||||
<value>Autoriser l'enregistrement d'écran ?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraTitle" xml:space="preserve">
|
||||
<value>Autoriser l'enregistrement de la caméra ?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenDescription" xml:space="preserve">
|
||||
<value>Un agent demande d'enregistrer votre écran. Cela capturera la vidéo de votre affichage et l'enverra à l'agent. Votre choix sera mémorisé pour les prochaines demandes jusqu'à ce que vous le modifiiez dans les Paramètres.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraDescription" xml:space="preserve">
|
||||
<value>Un agent demande d'enregistrer depuis votre caméra. Cela capturera la vidéo de votre webcam et l'enverra à l'agent. Votre choix sera mémorisé pour les prochaines demandes jusqu'à ce que vous le modifiiez dans les Paramètres.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Privacy" xml:space="preserve">
|
||||
<value>Vous pouvez modifier cela ultérieurement dans les Paramètres.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Allow" xml:space="preserve">
|
||||
<value>Autoriser l'enregistrement</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Deny" xml:space="preserve">
|
||||
<value>Refuser</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Settings: Privacy ==================== -->
|
||||
|
||||
<data name="Settings_PrivacyHeader" xml:space="preserve">
|
||||
<value>Confidentialité</value>
|
||||
</data>
|
||||
<data name="Settings_PrivacyDescription" xml:space="preserve">
|
||||
<value>Contrôlez quelles fonctionnalités les agents peuvent utiliser sur cet appareil.</value>
|
||||
</data>
|
||||
<data name="Settings_AllowScreenRecording" xml:space="preserve">
|
||||
<value>Autoriser l'enregistrement d'écran</value>
|
||||
</data>
|
||||
<data name="Settings_AllowCameraRecording" xml:space="preserve">
|
||||
<value>Autoriser l'enregistrement caméra</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyHeader.Text" xml:space="preserve">
|
||||
<value>Confidentialité</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyDescription.Text" xml:space="preserve">
|
||||
<value>Pré-approuvez les fonctionnalités afin que les agents puissent les utiliser sans demander la permission à chaque fois. Un compte à rebours s'affichera toujours avant l'enregistrement.</value>
|
||||
</data>
|
||||
<data name="ScreenRecordingToggle.Header" xml:space="preserve">
|
||||
<value>Autoriser l'enregistrement d'écran</value>
|
||||
</data>
|
||||
<data name="CameraRecordingToggle.Header" xml:space="preserve">
|
||||
<value>Autoriser l'enregistrement caméra</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Activity Stream: Recording ==================== -->
|
||||
|
||||
<data name="Activity_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>Enregistrement d'écran démarré</value>
|
||||
</data>
|
||||
<data name="Activity_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>Enregistrement d'écran terminé</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>Enregistrement caméra démarré</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>Enregistrement caméra terminé</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingRequestedByAgent" xml:space="preserve">
|
||||
<value>Enregistrement {0} demandé par l'agent</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingSentToAgent" xml:space="preserve">
|
||||
<value>Enregistrement {0} envoyé à l'agent</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Activity Stream Tip ==================== -->
|
||||
<data name="Toast_ActivityStreamTip" xml:space="preserve">
|
||||
<value>⚡ Nouveau: Fil d'activité</value>
|
||||
</data>
|
||||
@ -2572,4 +2682,4 @@ Sur votre hôte passerelle (Mac/Linux), exécutez :
|
||||
<data name="VoiceSettingsPage_PreviewVoiceButtonContent" xml:space="preserve">
|
||||
<value>▶ Aperçu de la voix</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@ -688,6 +688,116 @@ Gebruik een van deze opties:
|
||||
<data name="Toast_CameraBlockedDetail" xml:space="preserve">
|
||||
<value>Schakel cameratoegang in via Windows-privacyinstellingen voor OpenClaw Tray</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Recording ==================== -->
|
||||
|
||||
<data name="Toast_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 Schermopname gestart</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw-agent neemt uw scherm op</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>✅ Schermopname voltooid</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>Schermopname is naar de agent verzonden</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailed" xml:space="preserve">
|
||||
<value>❌ Schermopname mislukt</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailedDetail" xml:space="preserve">
|
||||
<value>Er is een fout opgetreden bij het opnemen van het scherm</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 Camera-opname gestart</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw-agent neemt op vanaf uw camera</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>✅ Camera-opname voltooid</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>Cameraclip is naar de agent verzonden</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Recording Consent Dialog ==================== -->
|
||||
|
||||
<data name="RecordingConsent_WindowTitle" xml:space="preserve">
|
||||
<value>OpenClaw · Toestemmingsverzoek</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenTitle" xml:space="preserve">
|
||||
<value>Schermopname toestaan?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraTitle" xml:space="preserve">
|
||||
<value>Camera-opname toestaan?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenDescription" xml:space="preserve">
|
||||
<value>Een agent vraagt om uw scherm op te nemen. Dit zal video van uw beeldscherm vastleggen en naar de agent sturen. Uw keuze wordt onthouden voor toekomstige verzoeken totdat u deze wijzigt in Instellingen.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraDescription" xml:space="preserve">
|
||||
<value>Een agent vraagt om op te nemen vanaf uw camera. Dit zal video van uw webcam vastleggen en naar de agent sturen. Uw keuze wordt onthouden voor toekomstige verzoeken totdat u deze wijzigt in Instellingen.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Privacy" xml:space="preserve">
|
||||
<value>U kunt dit later wijzigen in Instellingen.</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Allow" xml:space="preserve">
|
||||
<value>Opname toestaan</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Deny" xml:space="preserve">
|
||||
<value>Weigeren</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Settings: Privacy ==================== -->
|
||||
|
||||
<data name="Settings_PrivacyHeader" xml:space="preserve">
|
||||
<value>Privacyinstellingen</value>
|
||||
</data>
|
||||
<data name="Settings_PrivacyDescription" xml:space="preserve">
|
||||
<value>Bepaal welke mogelijkheden agents op dit apparaat kunnen gebruiken.</value>
|
||||
</data>
|
||||
<data name="Settings_AllowScreenRecording" xml:space="preserve">
|
||||
<value>Schermopname toestaan</value>
|
||||
</data>
|
||||
<data name="Settings_AllowCameraRecording" xml:space="preserve">
|
||||
<value>Camera-opname toestaan</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyHeader.Text" xml:space="preserve">
|
||||
<value>Privacyinstellingen</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyDescription.Text" xml:space="preserve">
|
||||
<value>Keur mogelijkheden vooraf goed zodat agents ze kunnen gebruiken zonder elke keer toestemming te vragen. Er wordt nog steeds een aftelling getoond voordat de opname begint.</value>
|
||||
</data>
|
||||
<data name="ScreenRecordingToggle.Header" xml:space="preserve">
|
||||
<value>Schermopname toestaan</value>
|
||||
</data>
|
||||
<data name="CameraRecordingToggle.Header" xml:space="preserve">
|
||||
<value>Camera-opname toestaan</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Activity Stream: Recording ==================== -->
|
||||
|
||||
<data name="Activity_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>Schermopname gestart</value>
|
||||
</data>
|
||||
<data name="Activity_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>Schermopname voltooid</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>Camera-opname gestart</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>Camera-opname voltooid</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingRequestedByAgent" xml:space="preserve">
|
||||
<value>{0}-opname aangevraagd door agent</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingSentToAgent" xml:space="preserve">
|
||||
<value>{0}-opname verzonden naar agent</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Activity Stream Tip ==================== -->
|
||||
<data name="Toast_ActivityStreamTip" xml:space="preserve">
|
||||
<value>⚡ Nieuw: Activiteitenstroom</value>
|
||||
</data>
|
||||
@ -2572,4 +2682,4 @@ Voer op uw gateway-host (Mac/Linux) uit:
|
||||
<data name="VoiceSettingsPage_PreviewVoiceButtonContent" xml:space="preserve">
|
||||
<value>▶ Voorbeeld van stem</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@ -688,6 +688,116 @@
|
||||
<data name="Toast_CameraBlockedDetail" xml:space="preserve">
|
||||
<value>请在 Windows 隐私设置中为 OpenClaw Tray 启用相机访问</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Recording ==================== -->
|
||||
|
||||
<data name="Toast_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 屏幕录制已开始</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw 代理正在录制您的屏幕</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>✅ 屏幕录制已完成</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>屏幕录制已发送给代理</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailed" xml:space="preserve">
|
||||
<value>❌ 屏幕录制失败</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailedDetail" xml:space="preserve">
|
||||
<value>录制屏幕时发生错误</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 摄像头录制已开始</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw 代理正在从您的摄像头录制</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>✅ 摄像头录制已完成</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>摄像头录制片段已发送给代理</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Recording Consent Dialog ==================== -->
|
||||
|
||||
<data name="RecordingConsent_WindowTitle" xml:space="preserve">
|
||||
<value>OpenClaw · 权限请求</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenTitle" xml:space="preserve">
|
||||
<value>允许屏幕录制?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraTitle" xml:space="preserve">
|
||||
<value>允许摄像头录制?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenDescription" xml:space="preserve">
|
||||
<value>代理正在请求录制您的屏幕。这将从您的显示器捕获视频并发送给代理。您的选择将被记住用于以后的请求,直到您在设置中更改。</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraDescription" xml:space="preserve">
|
||||
<value>代理正在请求从您的摄像头录制。这将从您的网络摄像头捕获视频并发送给代理。您的选择将被记住用于以后的请求,直到您在设置中更改。</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Privacy" xml:space="preserve">
|
||||
<value>您可以稍后在设置中更改此项。</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Allow" xml:space="preserve">
|
||||
<value>允许录制</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Deny" xml:space="preserve">
|
||||
<value>拒绝</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Settings: Privacy ==================== -->
|
||||
|
||||
<data name="Settings_PrivacyHeader" xml:space="preserve">
|
||||
<value>隐私</value>
|
||||
</data>
|
||||
<data name="Settings_PrivacyDescription" xml:space="preserve">
|
||||
<value>控制代理可以在此设备上使用哪些功能。</value>
|
||||
</data>
|
||||
<data name="Settings_AllowScreenRecording" xml:space="preserve">
|
||||
<value>允许屏幕录制</value>
|
||||
</data>
|
||||
<data name="Settings_AllowCameraRecording" xml:space="preserve">
|
||||
<value>允许摄像头录制</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyHeader.Text" xml:space="preserve">
|
||||
<value>隐私</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyDescription.Text" xml:space="preserve">
|
||||
<value>预先批准功能,以便代理无需每次都请求权限。录制开始前仍会显示倒计时。</value>
|
||||
</data>
|
||||
<data name="ScreenRecordingToggle.Header" xml:space="preserve">
|
||||
<value>允许屏幕录制</value>
|
||||
</data>
|
||||
<data name="CameraRecordingToggle.Header" xml:space="preserve">
|
||||
<value>允许摄像头录制</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Activity Stream: Recording ==================== -->
|
||||
|
||||
<data name="Activity_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>屏幕录制已开始</value>
|
||||
</data>
|
||||
<data name="Activity_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>屏幕录制已完成</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>摄像头录制已开始</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>摄像头录制已完成</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingRequestedByAgent" xml:space="preserve">
|
||||
<value>{0}录制由代理请求</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingSentToAgent" xml:space="preserve">
|
||||
<value>{0}录制已发送给代理</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Activity Stream Tip ==================== -->
|
||||
<data name="Toast_ActivityStreamTip" xml:space="preserve">
|
||||
<value>⚡ 新功能: 活动流</value>
|
||||
</data>
|
||||
@ -2572,4 +2682,4 @@
|
||||
<data name="VoiceSettingsPage_PreviewVoiceButtonContent" xml:space="preserve">
|
||||
<value>▶ 预览语音</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@ -688,6 +688,116 @@
|
||||
<data name="Toast_CameraBlockedDetail" xml:space="preserve">
|
||||
<value>請在 Windows 隱私設定中為 OpenClaw Tray 啟用相機訪問</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Recording ==================== -->
|
||||
|
||||
<data name="Toast_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 螢幕錄製已開始</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw 代理正在錄製您的螢幕</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>✅ 螢幕錄製已完成</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>螢幕錄製已傳送給代理</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailed" xml:space="preserve">
|
||||
<value>❌ 螢幕錄製失敗</value>
|
||||
</data>
|
||||
<data name="Toast_ScreenRecordingFailedDetail" xml:space="preserve">
|
||||
<value>錄製螢幕時發生錯誤</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>🔴 攝影機錄製已開始</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingStartedDetail" xml:space="preserve">
|
||||
<value>OpenClaw 代理正在從您的攝影機錄製</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>✅ 攝影機錄製已完成</value>
|
||||
</data>
|
||||
<data name="Toast_CameraRecordingCompleteDetail" xml:space="preserve">
|
||||
<value>攝影機錄製片段已傳送給代理</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Recording Consent Dialog ==================== -->
|
||||
|
||||
<data name="RecordingConsent_WindowTitle" xml:space="preserve">
|
||||
<value>OpenClaw · 權限請求</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenTitle" xml:space="preserve">
|
||||
<value>允許螢幕錄製?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraTitle" xml:space="preserve">
|
||||
<value>允許攝影機錄製?</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_ScreenDescription" xml:space="preserve">
|
||||
<value>代理正在請求錄製您的螢幕。這將從您的顯示器擷取視訊並傳送給代理。您的選擇將被記住用於以後的請求,直到您在設定中變更。</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_CameraDescription" xml:space="preserve">
|
||||
<value>代理正在請求從您的攝影機錄製。這將從您的網路攝影機擷取視訊並傳送給代理。您的選擇將被記住用於以後的請求,直到您在設定中變更。</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Privacy" xml:space="preserve">
|
||||
<value>您可以稍後在設定中變更此項。</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Allow" xml:space="preserve">
|
||||
<value>允許錄製</value>
|
||||
</data>
|
||||
<data name="RecordingConsent_Deny" xml:space="preserve">
|
||||
<value>拒絕</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Settings: Privacy ==================== -->
|
||||
|
||||
<data name="Settings_PrivacyHeader" xml:space="preserve">
|
||||
<value>隱私</value>
|
||||
</data>
|
||||
<data name="Settings_PrivacyDescription" xml:space="preserve">
|
||||
<value>控制代理可以在此裝置上使用哪些功能。</value>
|
||||
</data>
|
||||
<data name="Settings_AllowScreenRecording" xml:space="preserve">
|
||||
<value>允許螢幕錄製</value>
|
||||
</data>
|
||||
<data name="Settings_AllowCameraRecording" xml:space="preserve">
|
||||
<value>允許攝影機錄製</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyHeader.Text" xml:space="preserve">
|
||||
<value>隱私</value>
|
||||
</data>
|
||||
<data name="SettingsPrivacyDescription.Text" xml:space="preserve">
|
||||
<value>預先核准功能,以便代理無需每次都請求權限。錄製開始前仍會顯示倒數計時。</value>
|
||||
</data>
|
||||
<data name="ScreenRecordingToggle.Header" xml:space="preserve">
|
||||
<value>允許螢幕錄製</value>
|
||||
</data>
|
||||
<data name="CameraRecordingToggle.Header" xml:space="preserve">
|
||||
<value>允許攝影機錄製</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Activity Stream: Recording ==================== -->
|
||||
|
||||
<data name="Activity_ScreenRecordingStarted" xml:space="preserve">
|
||||
<value>螢幕錄製已開始</value>
|
||||
</data>
|
||||
<data name="Activity_ScreenRecordingComplete" xml:space="preserve">
|
||||
<value>螢幕錄製已完成</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingStarted" xml:space="preserve">
|
||||
<value>攝影機錄製已開始</value>
|
||||
</data>
|
||||
<data name="Activity_CameraRecordingComplete" xml:space="preserve">
|
||||
<value>攝影機錄製已完成</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingRequestedByAgent" xml:space="preserve">
|
||||
<value>{0}錄製由代理請求</value>
|
||||
</data>
|
||||
<data name="Activity_RecordingSentToAgent" xml:space="preserve">
|
||||
<value>{0}錄製已傳送給代理</value>
|
||||
</data>
|
||||
|
||||
<!-- ==================== Toast: Activity Stream Tip ==================== -->
|
||||
<data name="Toast_ActivityStreamTip" xml:space="preserve">
|
||||
<value>⚡ 新功能: 串流活動</value>
|
||||
</data>
|
||||
@ -2572,4 +2682,4 @@
|
||||
<data name="VoiceSettingsPage_PreviewVoiceButtonContent" xml:space="preserve">
|
||||
<value>▶ 預覽語音</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
109
tests/OpenClaw.Tray.Tests/ConsentAndSettingsSaveTests.cs
Normal file
109
tests/OpenClaw.Tray.Tests/ConsentAndSettingsSaveTests.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using OpenClawTray.Services;
|
||||
|
||||
namespace OpenClaw.Tray.Tests;
|
||||
|
||||
public class ConsentAndSettingsSaveTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Save_IsThreadSafe_ConcurrentCallsDoNotCorruptFile()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"openclaw-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var settings = new SettingsManager(tempDir);
|
||||
settings.GatewayUrl = "ws://localhost:9999";
|
||||
|
||||
// Fire many concurrent saves — none should throw or corrupt
|
||||
var tasks = Enumerable.Range(0, 20).Select(i =>
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
settings.ScreenRecordingConsentGiven = (i % 2 == 0);
|
||||
settings.Save();
|
||||
});
|
||||
}).ToArray();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Verify file is still valid JSON and loadable
|
||||
var reloaded = new SettingsManager(tempDir);
|
||||
Assert.Equal("ws://localhost:9999", reloaded.GatewayUrl);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_RaisesSavedEvent()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"openclaw-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var settings = new SettingsManager(tempDir);
|
||||
var eventRaised = false;
|
||||
settings.Saved += (s, e) => eventRaised = true;
|
||||
|
||||
settings.ScreenRecordingConsentGiven = true;
|
||||
settings.Save();
|
||||
|
||||
Assert.True(eventRaised);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsentFlags_PersistAcrossReload()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"openclaw-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var settings = new SettingsManager(tempDir);
|
||||
Assert.False(settings.ScreenRecordingConsentGiven);
|
||||
Assert.False(settings.CameraRecordingConsentGiven);
|
||||
|
||||
settings.ScreenRecordingConsentGiven = true;
|
||||
settings.CameraRecordingConsentGiven = true;
|
||||
settings.Save();
|
||||
|
||||
var reloaded = new SettingsManager(tempDir);
|
||||
Assert.True(reloaded.ScreenRecordingConsentGiven);
|
||||
Assert.True(reloaded.CameraRecordingConsentGiven);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsentFlags_CanBeRevoked()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"openclaw-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
try
|
||||
{
|
||||
var settings = new SettingsManager(tempDir);
|
||||
settings.ScreenRecordingConsentGiven = true;
|
||||
settings.Save();
|
||||
|
||||
// Revoke
|
||||
settings.ScreenRecordingConsentGiven = false;
|
||||
settings.Save();
|
||||
|
||||
var reloaded = new SettingsManager(tempDir);
|
||||
Assert.False(reloaded.ScreenRecordingConsentGiven);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,8 @@ public class SettingsRoundTripTests
|
||||
NodeCanvasEnabled = false,
|
||||
NodeScreenEnabled = true,
|
||||
NodeCameraEnabled = false,
|
||||
ScreenRecordingConsentGiven = true,
|
||||
CameraRecordingConsentGiven = true,
|
||||
NodeLocationEnabled = true,
|
||||
NodeBrowserProxyEnabled = false,
|
||||
NodeSttEnabled = true,
|
||||
@ -89,6 +91,8 @@ public class SettingsRoundTripTests
|
||||
Assert.Equal(original.NodeCanvasEnabled, restored.NodeCanvasEnabled);
|
||||
Assert.Equal(original.NodeScreenEnabled, restored.NodeScreenEnabled);
|
||||
Assert.Equal(original.NodeCameraEnabled, restored.NodeCameraEnabled);
|
||||
Assert.Equal(original.ScreenRecordingConsentGiven, restored.ScreenRecordingConsentGiven);
|
||||
Assert.Equal(original.CameraRecordingConsentGiven, restored.CameraRecordingConsentGiven);
|
||||
Assert.Equal(original.NodeLocationEnabled, restored.NodeLocationEnabled);
|
||||
Assert.Equal(original.NodeBrowserProxyEnabled, restored.NodeBrowserProxyEnabled);
|
||||
Assert.Equal(original.NodeSttEnabled, restored.NodeSttEnabled);
|
||||
@ -160,6 +164,8 @@ public class SettingsRoundTripTests
|
||||
Assert.True(settings.NodeCanvasEnabled);
|
||||
Assert.True(settings.NodeScreenEnabled);
|
||||
Assert.True(settings.NodeCameraEnabled);
|
||||
Assert.False(settings.ScreenRecordingConsentGiven);
|
||||
Assert.False(settings.CameraRecordingConsentGiven);
|
||||
Assert.True(settings.NodeLocationEnabled);
|
||||
Assert.True(settings.NodeBrowserProxyEnabled);
|
||||
Assert.False(settings.NodeSttEnabled);
|
||||
@ -231,6 +237,8 @@ public class SettingsRoundTripTests
|
||||
Assert.True(settings.NodeCanvasEnabled);
|
||||
Assert.True(settings.NodeScreenEnabled);
|
||||
Assert.True(settings.NodeCameraEnabled);
|
||||
Assert.False(settings.ScreenRecordingConsentGiven);
|
||||
Assert.False(settings.CameraRecordingConsentGiven);
|
||||
Assert.True(settings.NodeLocationEnabled);
|
||||
Assert.True(settings.NodeBrowserProxyEnabled);
|
||||
Assert.False(settings.NodeSttEnabled);
|
||||
@ -254,6 +262,32 @@ public class SettingsRoundTripTests
|
||||
Assert.Null(SettingsData.FromJson("not json at all"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsManager_PersistsRecordingConsentFlags()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "OpenClaw.Tray.Tests", Guid.NewGuid().ToString("N"));
|
||||
|
||||
try
|
||||
{
|
||||
var settings = new SettingsManager(dir)
|
||||
{
|
||||
ScreenRecordingConsentGiven = true,
|
||||
CameraRecordingConsentGiven = true
|
||||
};
|
||||
|
||||
settings.Save();
|
||||
|
||||
var reloaded = new SettingsManager(dir);
|
||||
Assert.True(reloaded.ScreenRecordingConsentGiven);
|
||||
Assert.True(reloaded.CameraRecordingConsentGiven);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[WindowsFact]
|
||||
public void SettingsManager_ProtectsElevenLabsApiKeyForStorage()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user