diff --git a/Controls/HistoryWorkspace.Actions.cs b/Controls/HistoryWorkspace.Actions.cs
index e23c668..4e567d1 100644
--- a/Controls/HistoryWorkspace.Actions.cs
+++ b/Controls/HistoryWorkspace.Actions.cs
@@ -105,7 +105,8 @@ private async void ExecuteAction(string action)
{
if (SelectedHistoryDevice is { } saved)
{
- var dialog = new HomeAssistantAdviceWindow(HomeAssistantText.FromHistory(saved), WebLink.Open, value => Clipboard.SetText(value));
+ var dialog = new HomeAssistantAdviceWindow(HomeAssistantText.FromHistory(saved), WebLink.Open,
+ value => Clipboard.SetText(value), surveyStore: SurveySource(StoreFor(_entry)));
dialog.Owner = Window.GetWindow(this); dialog.ShowDialog();
}
return;
diff --git a/Controls/HistoryWorkspace.xaml.cs b/Controls/HistoryWorkspace.xaml.cs
index ef39102..1799909 100644
--- a/Controls/HistoryWorkspace.xaml.cs
+++ b/Controls/HistoryWorkspace.xaml.cs
@@ -17,6 +17,7 @@ public partial class HistoryWorkspace : UserControl
/// The history the shown entry belongs to, which picks its baseline. Set by the shell.
internal Func HistorySource { get; set; } = () => ScanHistory.Store;
+ internal Func SurveySource { get; set; } = _ => null;
private HistoryStore StoreFor(ScanHistoryEntry? entry) => entry?.Owner ?? HistorySource();
private string Loc(string key) => TryFindResource(key) as string ?? key;
diff --git a/Controls/HomeAssistantAdviceWindow.cs b/Controls/HomeAssistantAdviceWindow.cs
index 93f9979..27043a6 100644
--- a/Controls/HomeAssistantAdviceWindow.cs
+++ b/Controls/HomeAssistantAdviceWindow.cs
@@ -20,9 +20,22 @@ internal sealed class HomeAssistantAdviceWindow : Window
{ ManufacturerModel = IdentityInput.Text, Protocol = ((ComboBoxItem?)ProtocolInput.SelectedItem)?.Tag as string ?? "",
Goal = ((ComboBoxItem?)GoalInput.SelectedItem)?.Tag as string ?? "Monitor" };
- internal HomeAssistantAdviceWindow(NetworkDevice device, Action open, Action copy)
+ internal HomeAssistantAdviceWindow(NetworkDevice device, Action open, Action copy,
+ HomeAssistantContext? initialContext = null, Action? apply = null, string? observation = null,
+ SurveyStore? surveyStore = null)
{
_device = HomeAssistantText.Freeze(device);
+ SurveyEntry? savedEntry = null;
+ if (apply == null && surveyStore != null)
+ {
+ try
+ {
+ string identity = SurveyStore.Mac(_device.MacAddress);
+ savedEntry = identity == "" ? null : surveyStore.Load().SingleOrDefault(entry => entry.LinkedMac == identity);
+ initialContext ??= savedEntry?.Context;
+ }
+ catch (Exception) { observation = SurveyText.L("Error"); }
+ }
_open = open; _copy = copy; CurrentAdvice = HomeAssistantAdvisor.ForDevice(_device);
Title = HomeAssistantText.L("Title"); Width = 920; Height = 760; MinWidth = 540; MinHeight = 380;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
@@ -36,13 +49,56 @@ internal HomeAssistantAdviceWindow(NetworkDevice device, Action open, Ac
copyPlan.Click += (_, _) => _copy(HomeAssistantText.Describe(_device, CurrentAdvice, CurrentContext));
var close = Button(Application.Current.TryFindResource("Str_Sys_Close") as string ?? "Close"); close.Click += (_, _) => Close();
buttons.Children.Add(catalogue); buttons.Children.Add(copyPlan); buttons.Children.Add(close);
+ var save = Button(apply == null ? SurveyText.L("Add") : SurveyText.L("Hints")); save.Tag = "survey-save";
+ save.Click += (_, _) =>
+ {
+ if (apply != null) { apply(CurrentContext); Close(); return; }
+ try
+ {
+ var store = surveyStore ?? throw new InvalidOperationException();
+ SurveyEntry? target = savedEntry;
+ string? confirmedIpChoice = null;
+ if (target == null)
+ {
+ string mac = SurveyStore.Mac(_device.MacAddress);
+ var candidates = store.Load().Where(entry => !entry.Manual && _device.IpAddress.Length > 0 &&
+ entry.Observation?.Device.IpAddress == _device.IpAddress && (entry.LinkedMac == "" || mac == "")).ToArray();
+ if (candidates.Length > 0)
+ {
+ confirmedIpChoice = SurveyPanel.Choose(this, candidates);
+ if (confirmedIpChoice == null) return;
+ target = candidates.SingleOrDefault(entry => entry.Id == confirmedIpChoice);
+ }
+ }
+ bool manual = savedEntry == null && _device.IpAddress.Length == 0 && SurveyStore.Mac(_device.MacAddress).Length == 0;
+ var draft = (target ?? new SurveyEntry { Name = manual ? "" : SurveyStore.DeviceName(_device), Manual = manual,
+ Observation = manual ? null : new SurveyObservation { Device = _device, LastSeenUtc = _device.Identification?.ObservationCompletedAtUtc } }) with
+ { ManufacturerModel = target?.ManufacturerModel ?? CurrentContext.ManufacturerModel,
+ Protocol = target?.Protocol ?? CurrentContext.Protocol, Goal = target?.Goal ?? CurrentContext.Goal };
+ if (savedEntry != null) draft = draft with { ManufacturerModel = CurrentContext.ManufacturerModel, Protocol = CurrentContext.Protocol, Goal = CurrentContext.Goal };
+ var editor = new SurveyEditorWindow(draft, entry =>
+ {
+ if (savedEntry != null) { store.Save(entry); savedEntry = entry; return; }
+ if (manual) { store.AddManual(entry); savedEntry = entry; return; }
+ var saved = store.AddObserved(_device, candidates => confirmedIpChoice ?? SurveyPanel.Choose(this, candidates), entry);
+ if (saved == null) throw new OperationCanceledException();
+ savedEntry = saved;
+ }, _open, _copy) { Owner = this };
+ editor.ShowDialog();
+ }
+ catch (Exception) { MessageBox.Show(this, SurveyText.L("Error"), SurveyText.L("Title"), MessageBoxButton.OK, MessageBoxImage.Error); }
+ };
+ if (apply != null || surveyStore != null) buttons.Children.Insert(0, save);
DockPanel.SetDock(buttons, Dock.Bottom); panel.Children.Add(buttons);
var header = new StackPanel();
string deviceLabel = (device.IpAddress + " " + device.Hostname).Trim();
header.Children.Add(Text(deviceLabel.Length == 0 ? HomeAssistantText.L("Workflow") : deviceLabel, 18));
+ if (observation != null) header.Children.Add(Text(observation));
var inputs = new StackPanel();
inputs.Children.Add(Text(HomeAssistantText.L("Identity"))); IdentityInput.SetResourceReference(StyleProperty, "DarkTextBox");
System.Windows.Automation.AutomationProperties.SetName(IdentityInput, HomeAssistantText.L("Identity")); inputs.Children.Add(IdentityInput);
+ ProtocolInput.SetResourceReference(StyleProperty, "DarkComboBox");
+ GoalInput.SetResourceReference(StyleProperty, "DarkComboBox");
var fields = new WrapPanel();
fields.Children.Add(Field(HomeAssistantText.L("Protocol"), ProtocolInput));
ProtocolInput.Items.Add(new ComboBoxItem { Content = Application.Current.TryFindResource("Str_Access_Unknown") as string ?? "Unknown", Tag = "" });
@@ -53,6 +109,12 @@ internal HomeAssistantAdviceWindow(NetworkDevice device, Action open, Ac
foreach (var (id, label) in new[] { ("Monitor", "Monitor"), ("Control", "ControlGoal"), ("Media", "MediaGoal") })
GoalInput.Items.Add(new ComboBoxItem { Content = HomeAssistantText.L(label), Tag = id });
GoalInput.SelectedIndex = 0; inputs.Children.Add(fields);
+ if (initialContext != null)
+ {
+ IdentityInput.Text = initialContext.ManufacturerModel;
+ ProtocolInput.SelectedItem = ProtocolInput.Items.Cast().FirstOrDefault(item => item.Tag as string == initialContext.Protocol) ?? ProtocolInput.Items[0];
+ GoalInput.SelectedItem = GoalInput.Items.Cast().FirstOrDefault(item => item.Tag as string == initialContext.Goal) ?? GoalInput.Items[0];
+ }
header.Children.Add(Expand(HomeAssistantText.L("Inputs"), inputs));
DockPanel.SetDock(header, Dock.Top); panel.Children.Add(header);
panel.Children.Add(new ScrollViewer { Content = Results, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled });
diff --git a/Controls/HomeAssistantOverviewWindow.cs b/Controls/HomeAssistantOverviewWindow.cs
index 5c211e0..b2a5d59 100644
--- a/Controls/HomeAssistantOverviewWindow.cs
+++ b/Controls/HomeAssistantOverviewWindow.cs
@@ -31,12 +31,16 @@ internal sealed class Row(NetworkDevice device)
}
internal IReadOnlyList Rows { get; }
internal readonly DataGrid Grid = new() { AutoGenerateColumns = false, IsReadOnly = true, CanUserAddRows = false,
- SelectionMode = DataGridSelectionMode.Single, HeadersVisibility = DataGridHeadersVisibility.Column,
+ SelectionMode = DataGridSelectionMode.Extended, HeadersVisibility = DataGridHeadersVisibility.Column,
RowHeaderWidth = 0, MinRowHeight = 54, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto };
private readonly Action _open, _copy;
- internal HomeAssistantOverviewWindow(IEnumerable devices, Action open, Action copy)
+ private readonly SurveyStore? _survey;
+
+ internal HomeAssistantOverviewWindow(IEnumerable devices, Action open, Action copy, LocationData? location = null)
{
_open = open; _copy = copy;
+ _survey = location?.Survey;
+ SurveyPanel.Theme(this);
Rows = devices.Select(HomeAssistantText.Freeze).OrderBy(device => device.IpSortKey).Select(device => new Row(device)).ToArray();
Title = HomeAssistantText.L("AllTitle"); Width = 1180; Height = 720; MinWidth = 660; MinHeight = 380;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
@@ -61,7 +65,7 @@ Button AddButton(string label, string tag, Action action)
button.SetResourceReference(StyleProperty, "OutlineButton"); button.Click += (_, _) => action(); buttons.Children.Add(button); return button;
}
var detail = AddButton(HomeAssistantText.L("Details"), "detail", OpenSelected); detail.IsEnabled = false;
- AddButton(HomeAssistantText.L("Copy"), "copy-all", () => _copy(Describe()));
+ var copyAll = AddButton(HomeAssistantText.L("Copy"), "copy-all", () => _copy(Describe()));
AddButton(Application.Current.TryFindResource("Str_Sys_Close") as string ?? "Close", "close", Close);
DockPanel.SetDock(buttons, Dock.Bottom); root.Children.Add(buttons);
void Column(string title, string path, double width)
@@ -79,10 +83,46 @@ void Column(string title, string path, double width)
Grid.ItemsSource = Rows;
Grid.SelectionChanged += (_, _) => detail.IsEnabled = Grid.SelectedItem is Row;
Grid.MouseDoubleClick += (_, e) => { if (ItemsControl.ContainerFromElement(Grid, e.OriginalSource as DependencyObject) is DataGridRow) OpenSelected(); };
- root.Children.Add(Grid); Content = root;
+ var scan = new DockPanel();
+ var tabs = new TabControl();
+ SurveyPanel? survey = null;
+ if (_survey != null)
+ {
+ survey = new SurveyPanel(_survey, Rows.Select(row => row.Device), open, copy);
+ var add = SurveyPanel.Button(SurveyText.L("Add"), () =>
+ {
+ var selected = Grid.SelectedItems.Cast().Select(row => row.Device).ToArray();
+ tabs.SelectedIndex = 1;
+ survey.AddSelected(selected);
+ }, "survey-add");
+ add.IsEnabled = false;
+ Grid.SelectionChanged += (_, _) => add.IsEnabled = Grid.SelectedItems.Count > 0;
+ DockPanel.SetDock(add, Dock.Bottom);
+ scan.Children.Add(add);
+ }
+ scan.Children.Add(Grid);
+ tabs.SetResourceReference(Control.BackgroundProperty, "BackgroundBrush");
+ tabs.SetResourceReference(Control.ForegroundProperty, "TextBrush");
+ tabs.Items.Add(new TabItem { Header = SurveyText.L("Scan"), Content = scan });
+ var unavailable = location?.IsTemporary == true
+ ? Application.Current.TryFindResource("Str_Loc_TemporaryNotice") as string ?? SurveyText.L("Error")
+ : SurveyText.L("Error");
+ tabs.Items.Add(new TabItem
+ {
+ Header = SurveyText.L("Title") + (location?.Info is { } info ? " · " + info.Name : ""),
+ Content = (FrameworkElement?)survey ?? SurveyPanel.Text(unavailable)
+ });
+ tabs.SelectionChanged += (_, args) =>
+ {
+ if (!ReferenceEquals(args.Source, tabs)) return;
+ detail.IsEnabled = tabs.SelectedIndex == 0 && Grid.SelectedItem is Row;
+ copyAll.IsEnabled = tabs.SelectedIndex == 0;
+ if (tabs.SelectedIndex == 1) survey?.Reload();
+ };
+ root.Children.Add(tabs); Content = root;
}
internal HomeAssistantAdviceWindow? CreateSelected() => Grid.SelectedItem is Row row
- ? new HomeAssistantAdviceWindow(row.Device, _open, _copy) : null;
+ ? new HomeAssistantAdviceWindow(row.Device, _open, _copy, surveyStore: _survey) : null;
private void OpenSelected()
{
if (CreateSelected() is not { } dialog) return;
diff --git a/Controls/ScanWorkspace/HomeAssistantActions.cs b/Controls/ScanWorkspace/HomeAssistantActions.cs
index 036a2ff..dc682ee 100644
--- a/Controls/ScanWorkspace/HomeAssistantActions.cs
+++ b/Controls/ScanWorkspace/HomeAssistantActions.cs
@@ -5,8 +5,8 @@ namespace KillerScan.Controls;
public partial class ScanWorkspace
{
private void HomeAssistantAll_Click(object sender, RoutedEventArgs e) { var window = CreateHomeAssistantOverview(); window.Owner = Window.GetWindow(this); window.ShowDialog(); }
- internal HomeAssistantOverviewWindow CreateHomeAssistantOverview() => new(ActiveDevices, WebLink.Open, CopyDeviceText);
- internal HomeAssistantAdviceWindow CreateHomeAssistantAdvice(NetworkDevice? device = null) => new(device ?? GetSelectedDevice() ?? new NetworkDevice(), WebLink.Open, CopyDeviceText);
+ internal HomeAssistantOverviewWindow CreateHomeAssistantOverview() => new(ActiveDevices, WebLink.Open, CopyDeviceText, _location);
+ internal HomeAssistantAdviceWindow CreateHomeAssistantAdvice(NetworkDevice? device = null) => new(device ?? GetSelectedDevice() ?? new NetworkDevice(), WebLink.Open, CopyDeviceText, surveyStore: _location.Survey);
private void HomeAssistant_Click(object sender, RoutedEventArgs e) { var window = CreateHomeAssistantAdvice(); window.Owner = Window.GetWindow(this); window.ShowDialog(); }
private void HomeAssistantRow_Click(object sender, RoutedEventArgs e)
{ if ((sender as FrameworkElement)?.DataContext is NetworkDevice device) { var window = CreateHomeAssistantAdvice(device); window.Owner = Window.GetWindow(this); window.ShowDialog(); } }
diff --git a/Controls/ScanWorkspace/Location.cs b/Controls/ScanWorkspace/Location.cs
index 411b71c..acc2dea 100644
--- a/Controls/ScanWorkspace/Location.cs
+++ b/Controls/ScanWorkspace/Location.cs
@@ -100,6 +100,12 @@ private bool SetLocation(LocationData data, bool keepResults)
if (!discard) return false;
}
if (clear) ClearResults();
+ if (keepResults)
+ foreach (var device in _active.Devices)
+ {
+ device.LocationId = data.Info?.Id;
+ device.LocationName = data.Info?.Name;
+ }
EnterLocation(data);
return true;
}
diff --git a/Controls/ScanWorkspace/Rescan.cs b/Controls/ScanWorkspace/Rescan.cs
index 04261b8..9747031 100644
--- a/Controls/ScanWorkspace/Rescan.cs
+++ b/Controls/ScanWorkspace/Rescan.cs
@@ -62,6 +62,8 @@ private async void RescanSelected_Click(object sender, RoutedEventArgs e)
var s = _active;
var scanner = s.Scanner;
+ var location = _location;
+ var surveyObservations = new List();
_rescanCts = new CancellationTokenSource();
var ct = _rescanCts.Token;
UpdateDeepScanButton();
@@ -92,7 +94,12 @@ private async void RescanSelected_Click(object sender, RoutedEventArgs e)
catch { done++; ScanProgress.Value = done * 100.0 / targets.Count; continue; }
int idx = s.Devices.IndexOf(old);
- if (idx >= 0) { s.Devices[idx] = fresh; refreshed++; }
+ if (idx >= 0)
+ {
+ s.Devices[idx] = fresh;
+ surveyObservations.Add(fresh);
+ refreshed++;
+ }
done++;
ScanProgress.Value = done * 100.0 / targets.Count;
@@ -105,6 +112,7 @@ private async void RescanSelected_Click(object sender, RoutedEventArgs e)
NoteRefresh(s, refreshed, targets.Count - refreshed, canceled);
_rescanCts.Dispose();
_rescanCts = null;
+ if (!LeaveDeletedLocation()) RefreshSurvey(location, surveyObservations);
FinishRescanUi();
}
}
@@ -138,6 +146,8 @@ private async Task DeepScanAllAsync(bool recordProfile = false)
var s = _active;
var scanner = s.Scanner;
+ var location = _location;
+ var surveyObservations = new List();
var targets = s.Devices.OrderBy(d => d.IpSortKey).ToList();
// The automatic deep phase belongs to the run that already validated its adapter.
if (!recordProfile)
@@ -173,6 +183,7 @@ private async Task DeepScanAllAsync(bool recordProfile = false)
if (idx >= 0)
{
s.Devices[idx] = fresh;
+ surveyObservations.Add(fresh);
refreshed++;
}
}
@@ -208,10 +219,11 @@ private async Task DeepScanAllAsync(bool recordProfile = false)
{
try
{
+ bool left = LeaveDeletedLocation();
+ if (!left) RefreshSurvey(location, surveyObservations);
if (recordProfile)
{
_scanCompleted = completion == ScanCompletion.Complete;
- LeaveDeletedLocation();
RecordCompletedRun(options, deep: true, completion);
}
// A manual Deep Scan All refreshes an existing snapshot; an automatic one is
diff --git a/Controls/ScanWorkspace/Scanning.cs b/Controls/ScanWorkspace/Scanning.cs
index c936480..a3448b4 100644
--- a/Controls/ScanWorkspace/Scanning.cs
+++ b/Controls/ScanWorkspace/Scanning.cs
@@ -97,7 +97,11 @@ private async void ScanBtn_Click(object sender, RoutedEventArgs e)
if (left) location = _location;
if (!runDeep || _active.Devices.Count == 0) RecordCompletedRun(options, deep: false);
NoteKnownDevices(location);
- if (!left) NoteUnsaved(location);
+ if (!left)
+ {
+ RefreshSurvey(location, _active.Devices);
+ NoteUnsaved(location);
+ }
}
}
catch (OperationCanceledException)
diff --git a/Controls/ScanWorkspace/SurveyActions.cs b/Controls/ScanWorkspace/SurveyActions.cs
new file mode 100644
index 0000000..c9e56f6
--- /dev/null
+++ b/Controls/ScanWorkspace/SurveyActions.cs
@@ -0,0 +1,18 @@
+using System.Windows;
+using KillerScan.Services;
+
+namespace KillerScan.Controls;
+
+public partial class ScanWorkspace
+{
+ private void RefreshSurvey(LocationData location, IEnumerable observed)
+ {
+ if (_disposed) return;
+ try { location.Survey?.Refresh(observed); }
+ catch (Exception)
+ {
+ // A survey write error must not turn a completed network scan into a failed scan.
+ if (!_disposed) MessageBox.Show(Window.GetWindow(this), SurveyText.L("Error"), SurveyText.L("Title"), MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+}
diff --git a/Controls/SurveyEditorWindow.cs b/Controls/SurveyEditorWindow.cs
new file mode 100644
index 0000000..fe66574
--- /dev/null
+++ b/Controls/SurveyEditorWindow.cs
@@ -0,0 +1,74 @@
+using System.Windows;
+using System.Windows.Controls;
+using KillerScan.Services;
+
+namespace KillerScan.Controls;
+
+internal sealed class SurveyEditorWindow : Window
+{
+ internal readonly Dictionary Inputs = new();
+ private SurveyEntry _draft;
+ private readonly Action _save;
+ private readonly Action _open, _copy;
+ internal SurveyEditorWindow(SurveyEntry draft, Action save, Action open, Action copy)
+ {
+ _draft = draft; _save = save; _open = open; _copy = copy;
+ SurveyPanel.Theme(this);
+ Title = SurveyText.L("Edit"); Width = 700; Height = 820; MinWidth = 420; MinHeight = 380;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ Loaded += (_, _) => KillerFamily.Windowing.WorkAreaFit.Fit(this);
+ SetResourceReference(BackgroundProperty, "BackgroundBrush"); SetResourceReference(ForegroundProperty, "TextBrush"); SetResourceReference(FontFamilyProperty, "UiFont");
+ var root = new DockPanel { Margin = new Thickness(18) };
+ var buttons = new WrapPanel { HorizontalAlignment = HorizontalAlignment.Right };
+ buttons.Children.Add(SurveyPanel.Button(SurveyText.L("Save"), () => { if (TrySave()) DialogResult = true; }, "save"));
+ var cancel = SurveyPanel.Button(SurveyText.L("Cancel"), Close, "cancel"); cancel.IsCancel = true; buttons.Children.Add(cancel);
+ DockPanel.SetDock(buttons, Dock.Bottom); root.Children.Add(buttons);
+ var fields = new StackPanel();
+ fields.Children.Add(SurveyPanel.Text(SurveyText.L("Notice")));
+ void Field(string key, string value, string? label = null, bool multiline = false)
+ {
+ fields.Children.Add(SurveyPanel.Text(label ?? SurveyText.L(key)));
+ var input = new TextBox { Text = value, MaxLength = key == "Name" ? 160 : 8000, MinHeight = multiline ? 65 : 30,
+ AcceptsReturn = multiline, TextWrapping = TextWrapping.Wrap, VerticalScrollBarVisibility = ScrollBarVisibility.Auto };
+ input.SetResourceReference(StyleProperty, "DarkTextBox");
+ System.Windows.Automation.AutomationProperties.SetName(input, label ?? SurveyText.L(key));
+ Inputs[key] = input; fields.Children.Add(input);
+ }
+ Field("Name", draft.Name); Field("Room", draft.Room);
+ Field("ManufacturerModel", draft.ManufacturerModel, HomeAssistantText.L("Identity"));
+ Field("Firmware", draft.Firmware); Field("Connection", draft.Connection);
+ Field("Functions", draft.DesiredFunctions, multiline: true); Field("Notes", draft.Notes, multiline: true);
+ var hints = SurveyPanel.Text("");
+ void UpdateHints() => hints.Text = HomeAssistantText.L("Protocol") + ": " +
+ (HomeAssistantAdvisor.Integration(_draft.Protocol)?.Name ?? Application.Current.TryFindResource("Str_Access_Unknown") as string ?? "Unknown") + " · " + HomeAssistantText.L("Goal") + ": " +
+ HomeAssistantText.L(_draft.Goal == "Control" ? "ControlGoal" : _draft.Goal == "Media" ? "MediaGoal" : "Monitor");
+ fields.Children.Add(hints); UpdateHints();
+ fields.Children.Add(SurveyPanel.Button(HomeAssistantText.L("Title"), () =>
+ {
+ var dialog = CreateAdvice();
+ dialog.Owner = this; dialog.ShowDialog();
+ UpdateHints();
+ }, "advice"));
+ fields.Children.Add(SurveyPanel.Text(SurveyPanel.ObservationText(draft)));
+ root.Children.Add(new ScrollViewer { Content = fields, VerticalScrollBarVisibility = ScrollBarVisibility.Auto }); Content = root;
+ }
+ internal HomeAssistantAdviceWindow CreateAdvice()
+ {
+ _draft = Draft();
+ return new HomeAssistantAdviceWindow(SurveyStore.AdviceDevice(_draft), _open, _copy, _draft.Context, context =>
+ {
+ _draft = _draft with { ManufacturerModel = context.ManufacturerModel, Protocol = context.Protocol, Goal = context.Goal };
+ Inputs["ManufacturerModel"].Text = context.ManufacturerModel;
+ }, SurveyPanel.ObservationText(_draft));
+ }
+ internal SurveyEntry Draft() => _draft with { Name = Inputs["Name"].Text.Trim(), Room = Inputs["Room"].Text,
+ ManufacturerModel = Inputs["ManufacturerModel"].Text, Firmware = Inputs["Firmware"].Text,
+ Connection = Inputs["Connection"].Text, DesiredFunctions = Inputs["Functions"].Text, Notes = Inputs["Notes"].Text };
+ internal bool TrySave()
+ {
+ if (string.IsNullOrWhiteSpace(Inputs["Name"].Text)) { MessageBox.Show(this, SurveyText.L("Required"), Title); return false; }
+ try { _save(Draft()); return true; }
+ catch (OperationCanceledException) { return false; }
+ catch (Exception) { MessageBox.Show(this, SurveyText.L("Error"), Title, MessageBoxButton.OK, MessageBoxImage.Error); return false; }
+ }
+}
diff --git a/Controls/SurveyPanel.cs b/Controls/SurveyPanel.cs
new file mode 100644
index 0000000..b8b5bc2
--- /dev/null
+++ b/Controls/SurveyPanel.cs
@@ -0,0 +1,129 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using KillerScan.Services;
+
+namespace KillerScan.Controls;
+
+internal sealed class SurveyPanel : DockPanel
+{
+ internal static void Theme(Window window) => window.Resources.MergedDictionaries.Add(new ResourceDictionary
+ { Source = new Uri("pack://application:,,,/KillerScan;component/Controls/SurveyStyles.xaml") });
+ private readonly SurveyStore _store;
+ private readonly Action _open, _copy;
+ private readonly NetworkDevice[] _devices;
+ internal readonly DataGrid Grid = new() { AutoGenerateColumns = false, IsReadOnly = true, CanUserAddRows = false,
+ SelectionMode = DataGridSelectionMode.Single, HeadersVisibility = DataGridHeadersVisibility.Column, MinRowHeight = 44 };
+ private readonly TextBlock _status = Text("");
+ private readonly List