From d486dce462f0b42ed5bcd2d3a89889704393f6c4 Mon Sep 17 00:00:00 2001 From: keyldev Date: Thu, 2 Jul 2026 21:46:53 +0300 Subject: [PATCH 1/2] feat(discovery): multi-add flow + OpenIPC web fingerprint - scan results, creds and deep-scan flag survive dialog reopen (session cache) - adding a camera loops back to the same list; "added" badge on known hosts - background Majestic fingerprint labels OpenIPC devices from any source - PingAsync: 401 on config.json counts as a hit + HTML fallback on "/" --- .../Services/DiscoveryDialogFactory.cs | 15 +++- .../Services/DiscoverySessionCache.cs | 39 ++++++++ src/OpenIPC.Viewer.App/Services/Localizer.cs | 2 + .../ViewModels/CameraLibraryPageViewModel.cs | 78 +++++++++------- .../Dialogs/DiscoveryDialogViewModel.cs | 90 ++++++++++++++++++- .../Dialogs/DiscoveryDialogContent.axaml | 16 +++- .../SharedComposition.cs | 1 + .../Discovery/SubnetSweepDiscoverySource.cs | 7 +- .../Majestic/MajesticHttpClient.cs | 27 +++++- 9 files changed, 232 insertions(+), 43 deletions(-) create mode 100644 src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs diff --git a/src/OpenIPC.Viewer.App/Services/DiscoveryDialogFactory.cs b/src/OpenIPC.Viewer.App/Services/DiscoveryDialogFactory.cs index d1586c4..939f00f 100644 --- a/src/OpenIPC.Viewer.App/Services/DiscoveryDialogFactory.cs +++ b/src/OpenIPC.Viewer.App/Services/DiscoveryDialogFactory.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using Microsoft.Extensions.Logging; using OpenIPC.Viewer.App.ViewModels.Dialogs; using OpenIPC.Viewer.Core.Discovery; +using OpenIPC.Viewer.Core.Majestic; using OpenIPC.Viewer.Core.Onvif; namespace OpenIPC.Viewer.App.Services; @@ -9,18 +11,27 @@ public sealed class DiscoveryDialogFactory { private readonly IDiscoveryAggregator _aggregator; private readonly OnvifProbeService _probe; + private readonly IMajesticClient _majestic; + private readonly DiscoverySessionCache _cache; private readonly ILoggerFactory _loggerFactory; public DiscoveryDialogFactory( IDiscoveryAggregator aggregator, OnvifProbeService probe, + IMajesticClient majestic, + DiscoverySessionCache cache, ILoggerFactory loggerFactory) { _aggregator = aggregator; _probe = probe; + _majestic = majestic; + _cache = cache; _loggerFactory = loggerFactory; } - public DiscoveryDialogViewModel Create() => - new(_aggregator, _probe, _loggerFactory.CreateLogger()); + // knownHosts: hosts of cameras already in the library, so rows can carry + // an "already added" badge during the multi-add loop. + public DiscoveryDialogViewModel Create(IReadOnlySet knownHosts) => + new(_aggregator, _probe, _majestic, _cache, knownHosts, + _loggerFactory.CreateLogger()); } diff --git a/src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs b/src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs new file mode 100644 index 0000000..b9632e1 --- /dev/null +++ b/src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using OpenIPC.Viewer.Core.Discovery; + +namespace OpenIPC.Viewer.App.Services; + +// Session-scoped memory for the discovery dialog, so the user can add several +// cameras from ONE scan: results, the typed credentials and the deep-scan flag +// all survive closing and reopening the dialog. In-memory only on purpose — +// the credentials must never be persisted to disk from here (they end up in +// the secrets store per-camera once a camera is saved). +public sealed class DiscoverySessionCache +{ + private readonly object _sync = new(); + private readonly Dictionary _devices = + new(StringComparer.OrdinalIgnoreCase); + + public string Username { get; set; } = ""; + public string Password { get; set; } = ""; + public bool DeepScan { get; set; } + + public IReadOnlyList Snapshot() + { + lock (_sync) + return new List(_devices.Values); + } + + public void Put(DiscoveredDevice device) + { + lock (_sync) + _devices[device.Host] = device; + } + + public void Clear() + { + lock (_sync) + _devices.Clear(); + } +} diff --git a/src/OpenIPC.Viewer.App/Services/Localizer.cs b/src/OpenIPC.Viewer.App/Services/Localizer.cs index 12083d1..46a7c25 100644 --- a/src/OpenIPC.Viewer.App/Services/Localizer.cs +++ b/src/OpenIPC.Viewer.App/Services/Localizer.cs @@ -396,6 +396,7 @@ private static LangCode DetectSystem() ["Discovery.Status.ManualAdd"] = "No ONVIF — review the stream URL in the editor.", ["Discovery.DeepScan"] = "Deep scan (slower)", ["Discovery.UnknownModel"] = "(unknown model)", + ["Discovery.AlreadyAdded"] = "added", ["Discovery.Confidence.Low"] = "low", ["Discovery.Confidence.Medium"] = "medium", ["Discovery.Confidence.High"] = "high", @@ -867,6 +868,7 @@ private static LangCode DetectSystem() ["Discovery.Status.ManualAdd"] = "Без ONVIF — проверьте URL потока в редакторе.", ["Discovery.DeepScan"] = "Глубокое сканирование (медленнее)", ["Discovery.UnknownModel"] = "(неизвестная модель)", + ["Discovery.AlreadyAdded"] = "добавлена", ["Discovery.Confidence.Low"] = "низкая", ["Discovery.Confidence.Medium"] = "средняя", ["Discovery.Confidence.High"] = "высокая", diff --git a/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs b/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs index 71f93ba..6ff371a 100644 --- a/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs +++ b/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs @@ -322,39 +322,53 @@ private async Task AddCameraAsync() [RelayCommand] private async Task DiscoverCameraAsync() { - var discoveryVm = _discoveryFactory.Create(); - var found = await _dialogs.ShowDiscoveryDialogAsync(discoveryVm).ConfigureAwait(true); - if (found is null) - return; - - // Pre-fill the editor from the probe result so the user sees / can tweak - // everything before saving (RTSP URI especially — phase-04 risks §"ONVIF - // returns wrong RTSP URI behind NAT" applies). - var editor = _editorFactory.CreateForNew(); - editor.Name = found.Device.Model ?? found.Device.Name ?? found.Device.Host; - editor.Host = found.Device.Host; - editor.OnvifPortText = (found.Device.OnvifServiceUri?.Port ?? 80).ToString(System.Globalization.CultureInfo.InvariantCulture); - editor.RtspMainText = found.RtspMainUri.ToString(); - editor.Username = found.Credentials?.Username ?? ""; - editor.Password = found.Credentials?.Password ?? ""; - - var result = await _dialogs.ShowCameraEditorAsync(editor).ConfigureAwait(true); - if (result?.NewRequest is not { } req) - return; - - try + // Multi-add loop: after each camera is saved (or its editor cancelled) + // the discovery dialog reopens with the SAME scan results and creds + // (DiscoverySessionCache), so several cameras go in from one scan. + // Only cancelling the discovery dialog itself exits. + var knownHosts = new System.Collections.Generic.HashSet( + System.Linq.Enumerable.Select(_allCameras, c => c.Host), + StringComparer.OrdinalIgnoreCase); + + while (true) { - var id = await _directory.AddAsync(req, CancellationToken.None).ConfigureAwait(true); - // Persist HasPtz / ProfileToken / manufacturer info from the probe so - // SingleCameraPage knows whether to show the PTZ joystick (Phase 4c). - // Non-ONVIF devices (sweep/mDNS) have no probe — nothing to persist. - if (found.Probe is { } probe) - await _directory.SaveOnvifMetadataAsync(id, probe, CancellationToken.None).ConfigureAwait(true); - await LoadAsync(CancellationToken.None).ConfigureAwait(true); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to add discovered camera {Host}", req.Host); + var discoveryVm = _discoveryFactory.Create(knownHosts); + var found = await _dialogs.ShowDiscoveryDialogAsync(discoveryVm).ConfigureAwait(true); + // Stop background fingerprints of the instance that just closed. + discoveryVm.Cancel(); + if (found is null) + return; + + // Pre-fill the editor from the probe result so the user sees / can tweak + // everything before saving (RTSP URI especially — phase-04 risks §"ONVIF + // returns wrong RTSP URI behind NAT" applies). + var editor = _editorFactory.CreateForNew(); + editor.Name = found.Device.Model ?? found.Device.Name ?? found.Device.Host; + editor.Host = found.Device.Host; + editor.OnvifPortText = (found.Device.OnvifServiceUri?.Port ?? 80).ToString(System.Globalization.CultureInfo.InvariantCulture); + editor.RtspMainText = found.RtspMainUri.ToString(); + editor.Username = found.Credentials?.Username ?? ""; + editor.Password = found.Credentials?.Password ?? ""; + + var result = await _dialogs.ShowCameraEditorAsync(editor).ConfigureAwait(true); + if (result?.NewRequest is not { } req) + continue; // editor cancelled — back to the scan list + + try + { + var id = await _directory.AddAsync(req, CancellationToken.None).ConfigureAwait(true); + // Persist HasPtz / ProfileToken / manufacturer info from the probe so + // SingleCameraPage knows whether to show the PTZ joystick (Phase 4c). + // Non-ONVIF devices (sweep/mDNS) have no probe — nothing to persist. + if (found.Probe is { } probe) + await _directory.SaveOnvifMetadataAsync(id, probe, CancellationToken.None).ConfigureAwait(true); + await LoadAsync(CancellationToken.None).ConfigureAwait(true); + knownHosts.Add(req.Host); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add discovered camera {Host}", req.Host); + } } } diff --git a/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs b/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs index 83acda9..73e7390 100644 --- a/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs +++ b/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs @@ -23,11 +23,19 @@ public sealed partial class DiscoveryDialogViewModel : ViewModelBase { private readonly IDiscoveryAggregator _aggregator; private readonly OnvifProbeService _probe; + private readonly OpenIPC.Viewer.Core.Majestic.IMajesticClient _majestic; + private readonly DiscoverySessionCache _cache; + private readonly IReadOnlySet _knownHosts; private readonly ILogger _logger; private CancellationTokenSource? _scanCts; + // Cancels in-flight Majestic fingerprints when the dialog goes away. + private readonly CancellationTokenSource _lifetimeCts = new(); private readonly Dictionary _rowsByHost = new(StringComparer.OrdinalIgnoreCase); + // Hosts we already fingerprinted (or are fingerprinting) — one ping per host. + private readonly HashSet _fingerprinted = new(StringComparer.OrdinalIgnoreCase); + private readonly SemaphoreSlim _fingerprintGate = new(6); public ObservableCollection Cameras { get; } = new(); @@ -66,18 +74,40 @@ public sealed partial class DiscoveryDialogViewModel : ViewModelBase public DiscoveryDialogViewModel( IDiscoveryAggregator aggregator, OnvifProbeService probe, + OpenIPC.Viewer.Core.Majestic.IMajesticClient majestic, + DiscoverySessionCache cache, + IReadOnlySet knownHosts, ILogger logger) { _aggregator = aggregator; _probe = probe; + _majestic = majestic; + _cache = cache; + _knownHosts = knownHosts; _logger = logger; + + // Rehydrate the previous scan so the user can add several cameras + // one-by-one without rescanning between dialog opens. + _username = cache.Username; + _password = cache.Password; + _deepScan = cache.DeepScan; + foreach (var device in cache.Snapshot()) + Upsert(device); + if (Cameras.Count > 0) + StatusText = string.Format(Localizer.Instance["Discovery.Status.FoundFormat"], Cameras.Count); } + partial void OnUsernameChanged(string value) => _cache.Username = value; + partial void OnPasswordChanged(string value) => _cache.Password = value; + partial void OnDeepScanChanged(bool value) => _cache.DeepScan = value; + [RelayCommand(CanExecute = nameof(CanScan))] private async Task ScanAsync() { Cameras.Clear(); _rowsByHost.Clear(); + _fingerprinted.Clear(); + _cache.Clear(); Selected = null; ScanProgress = 0; StatusText = Localizer.Instance["Discovery.Status.Scanning"]; @@ -125,10 +155,63 @@ private void Upsert(DiscoveredDevice device) } else { - row = new DiscoveredDeviceRowVm(device); + row = new DiscoveredDeviceRowVm(device) + { + IsAlreadyAdded = _knownHosts.Contains(device.Host), + }; _rowsByHost[device.Host] = row; Cameras.Add(row); } + + _cache.Put(row.Device); + ScheduleFingerprint(row.Device); + } + + // Newer OpenIPC firmwares always run the Majestic web UI, so an HTTP ping + // identifies them even when they answer neither ONVIF nor mDNS with a + // model. One bounded background ping per host; on a hit the row upgrades + // in place (Majestic protocol + "OpenIPC" label + High confidence). + private void ScheduleFingerprint(DiscoveredDevice device) + { + if (device.Protocols.HasFlag(DiscoveryProtocol.Majestic) && device.Model is not null) + return; + if (!_fingerprinted.Add(device.Host)) + return; + + var ct = _lifetimeCts.Token; + _ = Task.Run(async () => + { + await _fingerprintGate.WaitAsync(ct).ConfigureAwait(false); + try + { + var hit = device.Protocols.HasFlag(DiscoveryProtocol.Majestic); + var ports = device.Ports.Where(p => p is 80 or 8080).DefaultIfEmpty(80); + foreach (var port in ports) + { + if (hit) break; + hit = await _majestic.PingAsync( + new OpenIPC.Viewer.Core.Majestic.MajesticEndpoint(device.Host, port, null), ct).ConfigureAwait(false); + } + if (!hit) return; + + await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => + { + if (!_rowsByHost.TryGetValue(device.Host, out var row)) return; + row.Device = row.Device.MergeWith(new DiscoveredDevice( + device.Host, DiscoveryProtocol.Majestic, Array.Empty(), Model: "OpenIPC")); + _cache.Put(row.Device); + }); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + _logger.LogDebug(ex, "Majestic fingerprint failed for {Host}", device.Host); + } + finally + { + _fingerprintGate.Release(); + } + }, ct); } private bool CanScan() => !ScanInProgress && !AddInProgress; @@ -199,6 +282,7 @@ private static Uri GuessRtspUri(DiscoveredDevice device) public void Cancel() { _scanCts?.Cancel(); + _lifetimeCts.Cancel(); } } @@ -212,6 +296,10 @@ public sealed partial class DiscoveredDeviceRowVm : ViewModelBase [NotifyPropertyChangedFor(nameof(ConfidenceText))] private DiscoveredDevice _device; + // A camera with this host already exists in the library — shown as a badge + // so the multi-add flow makes it obvious what's left to add. + [ObservableProperty] private bool _isAlreadyAdded; + public DiscoveredDeviceRowVm(DiscoveredDevice device) => _device = device; public string Host => Device.Host; diff --git a/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml b/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml index b7d7078..4247741 100644 --- a/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml +++ b/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml @@ -62,9 +62,19 @@ - + + + + + + diff --git a/src/OpenIPC.Viewer.Composition/SharedComposition.cs b/src/OpenIPC.Viewer.Composition/SharedComposition.cs index 14f97e1..6b7eeda 100644 --- a/src/OpenIPC.Viewer.Composition/SharedComposition.cs +++ b/src/OpenIPC.Viewer.Composition/SharedComposition.cs @@ -167,6 +167,7 @@ public static IServiceCollection AddSharedServices(this IServiceCollection servi services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/OpenIPC.Viewer.Devices/Discovery/SubnetSweepDiscoverySource.cs b/src/OpenIPC.Viewer.Devices/Discovery/SubnetSweepDiscoverySource.cs index bf9d471..fab2c47 100644 --- a/src/OpenIPC.Viewer.Devices/Discovery/SubnetSweepDiscoverySource.cs +++ b/src/OpenIPC.Viewer.Devices/Discovery/SubnetSweepDiscoverySource.cs @@ -143,7 +143,12 @@ public async IAsyncEnumerable ScanAsync( } } - return protocols == DiscoveryProtocol.None ? null : new DiscoveredDevice(host, protocols, ports); + if (protocols == DiscoveryProtocol.None) + return null; + // A positive fingerprint names the device — the dialog shows the label + // instead of "(unknown model)". + var model = protocols.HasFlag(DiscoveryProtocol.Majestic) ? "OpenIPC" : null; + return new DiscoveredDevice(host, protocols, ports, Model: model); } private static bool TryGetPrefix(string ip, out string prefix, out int lastOctet) diff --git a/src/OpenIPC.Viewer.Devices/Majestic/MajesticHttpClient.cs b/src/OpenIPC.Viewer.Devices/Majestic/MajesticHttpClient.cs index 78e1c53..be6ced0 100644 --- a/src/OpenIPC.Viewer.Devices/Majestic/MajesticHttpClient.cs +++ b/src/OpenIPC.Viewer.Devices/Majestic/MajesticHttpClient.cs @@ -43,10 +43,29 @@ public async Task PingAsync(MajesticEndpoint endpoint, CancellationToken c // ""), which fails the JSON sniff and hides an // otherwise working Majestic. config.json is the canonical endpoint // and always returns the live config object. - using var resp = await SendAsync(endpoint, HttpMethod.Get, "api/v1/config.json", ct).ConfigureAwait(false); - if (!resp.IsSuccessStatusCode) return false; - var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - return LooksLikeMajesticConfig(body); + using (var resp = await SendAsync(endpoint, HttpMethod.Get, "api/v1/config.json", ct).ConfigureAwait(false)) + { + // Newer firmwares guard the API behind auth — a 401 challenge on + // this exact path still means "a Majestic is answering here". + if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) + return true; + if (resp.IsSuccessStatusCode) + { + var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + if (LooksLikeMajesticConfig(body)) + return true; + } + } + + // Fallback for firmwares where the JSON API moved or is disabled but + // the web UI is up: the index page names OpenIPC/Majestic in its HTML. + using (var resp = await SendAsync(endpoint, HttpMethod.Get, "", ct).ConfigureAwait(false)) + { + if (!resp.IsSuccessStatusCode) return false; + var html = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + return html.Contains("openipc", StringComparison.OrdinalIgnoreCase) + || html.Contains("majestic", StringComparison.OrdinalIgnoreCase); + } } catch (Exception ex) { From 5581aa3c55f5851c2497956a555940dacd26989d Mon Sep 17 00:00:00 2001 From: keyldev Date: Thu, 2 Jul 2026 21:55:41 +0300 Subject: [PATCH 2/2] feat(discovery): "use these credentials for all cameras" toggle - on (default): typed login carries into the next add - off: every camera starts with blank fields (mixed-credential parks) --- .../Services/DiscoverySessionCache.cs | 5 ++++ src/OpenIPC.Viewer.App/Services/Localizer.cs | 2 ++ .../Dialogs/DiscoveryDialogViewModel.cs | 17 +++++++++-- .../Dialogs/DiscoveryDialogContent.axaml | 28 +++++++++++-------- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs b/src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs index b9632e1..0ae86ca 100644 --- a/src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs +++ b/src/OpenIPC.Viewer.App/Services/DiscoverySessionCache.cs @@ -19,6 +19,11 @@ public sealed class DiscoverySessionCache public string Password { get; set; } = ""; public bool DeepScan { get; set; } + // "Use these credentials for all cameras" — on (default) carries the typed + // login into the next add; off makes every camera start with blank fields + // (mixed-credential parks). + public bool ReuseCredentials { get; set; } = true; + public IReadOnlyList Snapshot() { lock (_sync) diff --git a/src/OpenIPC.Viewer.App/Services/Localizer.cs b/src/OpenIPC.Viewer.App/Services/Localizer.cs index 46a7c25..19ff488 100644 --- a/src/OpenIPC.Viewer.App/Services/Localizer.cs +++ b/src/OpenIPC.Viewer.App/Services/Localizer.cs @@ -397,6 +397,7 @@ private static LangCode DetectSystem() ["Discovery.DeepScan"] = "Deep scan (slower)", ["Discovery.UnknownModel"] = "(unknown model)", ["Discovery.AlreadyAdded"] = "added", + ["Discovery.ReuseCreds"] = "Use these credentials for all cameras", ["Discovery.Confidence.Low"] = "low", ["Discovery.Confidence.Medium"] = "medium", ["Discovery.Confidence.High"] = "high", @@ -869,6 +870,7 @@ private static LangCode DetectSystem() ["Discovery.DeepScan"] = "Глубокое сканирование (медленнее)", ["Discovery.UnknownModel"] = "(неизвестная модель)", ["Discovery.AlreadyAdded"] = "добавлена", + ["Discovery.ReuseCreds"] = "Одни учётные данные для всех камер", ["Discovery.Confidence.Low"] = "низкая", ["Discovery.Confidence.Medium"] = "средняя", ["Discovery.Confidence.High"] = "высокая", diff --git a/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs b/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs index 73e7390..5e91a0f 100644 --- a/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs +++ b/src/OpenIPC.Viewer.App/ViewModels/Dialogs/DiscoveryDialogViewModel.cs @@ -51,6 +51,10 @@ public sealed partial class DiscoveryDialogViewModel : ViewModelBase [ObservableProperty] private string _username = ""; [ObservableProperty] private string _password = ""; + // "Use these credentials for all cameras" (multi-add). Off → the next + // dialog instance starts with blank login fields. + [ObservableProperty] private bool _reuseCredentials = true; + // 0..1 scan progress (mean across sources). Drives the progress bar; hidden // when not scanning. [ObservableProperty] private double _scanProgress; @@ -87,9 +91,15 @@ public DiscoveryDialogViewModel( _logger = logger; // Rehydrate the previous scan so the user can add several cameras - // one-by-one without rescanning between dialog opens. - _username = cache.Username; - _password = cache.Password; + // one-by-one without rescanning between dialog opens. Credentials only + // carry over while "use for all cameras" is on — a mixed-credential + // park starts each camera with blank fields. + _reuseCredentials = cache.ReuseCredentials; + if (_reuseCredentials) + { + _username = cache.Username; + _password = cache.Password; + } _deepScan = cache.DeepScan; foreach (var device in cache.Snapshot()) Upsert(device); @@ -100,6 +110,7 @@ public DiscoveryDialogViewModel( partial void OnUsernameChanged(string value) => _cache.Username = value; partial void OnPasswordChanged(string value) => _cache.Password = value; partial void OnDeepScanChanged(bool value) => _cache.DeepScan = value; + partial void OnReuseCredentialsChanged(bool value) => _cache.ReuseCredentials = value; [RelayCommand(CanExecute = nameof(CanScan))] private async Task ScanAsync() diff --git a/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml b/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml index 4247741..849bf30 100644 --- a/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml +++ b/src/OpenIPC.Viewer.App/Views/Dialogs/DiscoveryDialogContent.axaml @@ -99,17 +99,23 @@ - - - - - - - - - - + + + + + + + + + + + + +