mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 10:46:33 +02:00
[Hosts]Warn about duplicated entries (#22606)
* find duplicated entries * addressed PR feedback Co-authored-by: Davide <25966642+davidegiacometti@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b56e62e5de
commit
5b4e678f14
@@ -35,6 +35,7 @@ namespace Hosts.Models
|
|||||||
{
|
{
|
||||||
SetProperty(ref _hosts, value);
|
SetProperty(ref _hosts, value);
|
||||||
OnPropertyChanged(nameof(Valid));
|
OnPropertyChanged(nameof(Valid));
|
||||||
|
SplittedHosts = _hosts.Split(' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +51,13 @@ namespace Hosts.Models
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _pinging;
|
private bool _pinging;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _duplicate;
|
||||||
|
|
||||||
public bool Valid => ValidationHelper.ValidHosts(_hosts) && (ValidationHelper.ValidIPv4(_address) || ValidationHelper.ValidIPv6(_address));
|
public bool Valid => ValidationHelper.ValidHosts(_hosts) && (ValidationHelper.ValidIPv4(_address) || ValidationHelper.ValidIPv6(_address));
|
||||||
|
|
||||||
|
public string[] SplittedHosts { get; private set; }
|
||||||
|
|
||||||
public Entry()
|
public Entry()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,9 @@
|
|||||||
<data name="DeleteDialogAreYouSure.Text" xml:space="preserve">
|
<data name="DeleteDialogAreYouSure.Text" xml:space="preserve">
|
||||||
<value>Are you sure you want to delete this entry?</value>
|
<value>Are you sure you want to delete this entry?</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DuplicateEntryIcon.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||||
|
<value>Duplicate entry</value>
|
||||||
|
</data>
|
||||||
<data name="Entries.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
<data name="Entries.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
<value>Entries</value>
|
<value>Entries</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -234,6 +237,10 @@
|
|||||||
<value>Ping</value>
|
<value>Ping</value>
|
||||||
<comment>"Ping" refers to the command-line utility, do not loc</comment>
|
<comment>"Ping" refers to the command-line utility, do not loc</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PingIcon.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||||
|
<value>Ping response</value>
|
||||||
|
<comment>"Ping" refers to the command-line utility, do not loc</comment>
|
||||||
|
</data>
|
||||||
<data name="Reload.Content" xml:space="preserve">
|
<data name="Reload.Content" xml:space="preserve">
|
||||||
<value>Reload</value>
|
<value>Reload</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -243,6 +250,9 @@
|
|||||||
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||||
<value>Settings</value>
|
<value>Settings</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ShowOnlyDuplicates.Header" xml:space="preserve">
|
||||||
|
<value>Show only duplicates</value>
|
||||||
|
</data>
|
||||||
<data name="UpdateBtn" xml:space="preserve">
|
<data name="UpdateBtn" xml:space="preserve">
|
||||||
<value>Update</value>
|
<value>Update</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// Copyright (c) Microsoft Corporation
|
// Copyright (c) Microsoft Corporation
|
||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -13,7 +14,6 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using Hosts.Helpers;
|
using Hosts.Helpers;
|
||||||
using Hosts.Models;
|
using Hosts.Models;
|
||||||
using Hosts.Settings;
|
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
|
|
||||||
namespace Hosts.ViewModels
|
namespace Hosts.ViewModels
|
||||||
@@ -21,7 +21,6 @@ namespace Hosts.ViewModels
|
|||||||
public partial class MainViewModel : ObservableObject, IDisposable
|
public partial class MainViewModel : ObservableObject, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IHostsService _hostsService;
|
private readonly IHostsService _hostsService;
|
||||||
private readonly IUserSettings _userSettings;
|
|
||||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
@@ -34,6 +33,9 @@ namespace Hosts.ViewModels
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _fileChanged;
|
private bool _fileChanged;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _filtered;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _addressFilter;
|
private string _addressFilter;
|
||||||
|
|
||||||
@@ -44,50 +46,15 @@ namespace Hosts.ViewModels
|
|||||||
private string _commentFilter;
|
private string _commentFilter;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _filtered;
|
[NotifyPropertyChangedFor(nameof(Entries))]
|
||||||
|
private bool _showOnlyDuplicates;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _additionalLines;
|
private string _additionalLines;
|
||||||
|
|
||||||
private ObservableCollection<Entry> _entries;
|
private ObservableCollection<Entry> _entries;
|
||||||
|
|
||||||
public ObservableCollection<Entry> Entries
|
public ObservableCollection<Entry> Entries => _filtered || _showOnlyDuplicates ? GetFilteredEntries() : _entries;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_filtered)
|
|
||||||
{
|
|
||||||
var filter = _entries.AsEnumerable();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_addressFilter))
|
|
||||||
{
|
|
||||||
filter = filter.Where(e => e.Address.Contains(_addressFilter, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_hostsFilter))
|
|
||||||
{
|
|
||||||
filter = filter.Where(e => e.Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_commentFilter))
|
|
||||||
{
|
|
||||||
filter = filter.Where(e => e.Comment.Contains(_commentFilter, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ObservableCollection<Entry>(filter);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return _entries;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_entries = value;
|
|
||||||
OnPropertyChanged(nameof(Entries));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICommand ReadHostsCommand => new RelayCommand(ReadHosts);
|
public ICommand ReadHostsCommand => new RelayCommand(ReadHosts);
|
||||||
|
|
||||||
@@ -99,12 +66,9 @@ namespace Hosts.ViewModels
|
|||||||
|
|
||||||
public ICommand OpenHostsFileCommand => new RelayCommand(OpenHostsFile);
|
public ICommand OpenHostsFileCommand => new RelayCommand(OpenHostsFile);
|
||||||
|
|
||||||
public MainViewModel(
|
public MainViewModel(IHostsService hostService)
|
||||||
IHostsService hostService,
|
|
||||||
IUserSettings userSettings)
|
|
||||||
{
|
{
|
||||||
_hostsService = hostService;
|
_hostsService = hostService;
|
||||||
_userSettings = userSettings;
|
|
||||||
|
|
||||||
_hostsService.FileChanged += (s, e) =>
|
_hostsService.FileChanged += (s, e) =>
|
||||||
{
|
{
|
||||||
@@ -116,24 +80,35 @@ namespace Hosts.ViewModels
|
|||||||
{
|
{
|
||||||
entry.PropertyChanged += Entry_PropertyChanged;
|
entry.PropertyChanged += Entry_PropertyChanged;
|
||||||
_entries.Add(entry);
|
_entries.Add(entry);
|
||||||
|
|
||||||
|
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||||
|
OnPropertyChanged(nameof(Entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(int index, Entry entry)
|
public void Update(int index, Entry entry)
|
||||||
{
|
{
|
||||||
var existingEntry = _entries.ElementAt(index);
|
var existingEntry = Entries.ElementAt(index);
|
||||||
|
var oldAddress = existingEntry.Address;
|
||||||
|
var oldHosts = existingEntry.SplittedHosts;
|
||||||
|
|
||||||
existingEntry.Address = entry.Address;
|
existingEntry.Address = entry.Address;
|
||||||
existingEntry.Comment = entry.Comment;
|
existingEntry.Comment = entry.Comment;
|
||||||
existingEntry.Hosts = entry.Hosts;
|
existingEntry.Hosts = entry.Hosts;
|
||||||
existingEntry.Active = entry.Active;
|
existingEntry.Active = entry.Active;
|
||||||
|
|
||||||
|
FindDuplicates(oldAddress, oldHosts);
|
||||||
|
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||||
|
OnPropertyChanged(nameof(Entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteSelected()
|
public void DeleteSelected()
|
||||||
{
|
{
|
||||||
|
var address = Selected.Address;
|
||||||
|
var hosts = Selected.SplittedHosts;
|
||||||
_entries.Remove(Selected);
|
_entries.Remove(Selected);
|
||||||
if (Filtered)
|
|
||||||
{
|
FindDuplicates(address, hosts);
|
||||||
OnPropertyChanged(nameof(Entries));
|
OnPropertyChanged(nameof(Entries));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateAdditionalLines(string lines)
|
public void UpdateAdditionalLines(string lines)
|
||||||
@@ -157,7 +132,7 @@ namespace Hosts.ViewModels
|
|||||||
|
|
||||||
await _dispatcherQueue.EnqueueAsync(() =>
|
await _dispatcherQueue.EnqueueAsync(() =>
|
||||||
{
|
{
|
||||||
Entries = new ObservableCollection<Entry>(entries);
|
_entries = new ObservableCollection<Entry>(entries);
|
||||||
|
|
||||||
foreach (var e in _entries)
|
foreach (var e in _entries)
|
||||||
{
|
{
|
||||||
@@ -165,17 +140,24 @@ namespace Hosts.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
_entries.CollectionChanged += Entries_CollectionChanged;
|
_entries.CollectionChanged += Entries_CollectionChanged;
|
||||||
|
OnPropertyChanged(nameof(Entries));
|
||||||
|
FindDuplicates();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyFilters()
|
public void ApplyFilters()
|
||||||
{
|
{
|
||||||
if (_entries != null)
|
if (_entries == null)
|
||||||
{
|
{
|
||||||
Filtered = !string.IsNullOrWhiteSpace(_addressFilter) || !string.IsNullOrWhiteSpace(_hostsFilter) || !string.IsNullOrWhiteSpace(_commentFilter);
|
return;
|
||||||
OnPropertyChanged(nameof(Entries));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Filtered = !string.IsNullOrWhiteSpace(_addressFilter)
|
||||||
|
|| !string.IsNullOrWhiteSpace(_hostsFilter)
|
||||||
|
|| !string.IsNullOrWhiteSpace(_commentFilter);
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(Entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearFilters()
|
public void ClearFilters()
|
||||||
@@ -183,6 +165,7 @@ namespace Hosts.ViewModels
|
|||||||
AddressFilter = null;
|
AddressFilter = null;
|
||||||
HostsFilter = null;
|
HostsFilter = null;
|
||||||
CommentFilter = null;
|
CommentFilter = null;
|
||||||
|
ShowOnlyDuplicates = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PingSelectedAsync()
|
public async Task PingSelectedAsync()
|
||||||
@@ -212,8 +195,10 @@ namespace Hosts.ViewModels
|
|||||||
|
|
||||||
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
// Ping should't trigger a file save
|
// Ping and duplicate should't trigger a file save
|
||||||
if (e.PropertyName == nameof(Entry.Ping) || e.PropertyName == nameof(Entry.Pinging))
|
if (e.PropertyName == nameof(Entry.Ping)
|
||||||
|
|| e.PropertyName == nameof(Entry.Pinging)
|
||||||
|
|| e.PropertyName == nameof(Entry.Duplicate))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -234,6 +219,68 @@ namespace Hosts.ViewModels
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FindDuplicates()
|
||||||
|
{
|
||||||
|
foreach (var entry in _entries)
|
||||||
|
{
|
||||||
|
SetDuplicate(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FindDuplicates(string address, IEnumerable<string> hosts)
|
||||||
|
{
|
||||||
|
var entries = _entries.Where(e =>
|
||||||
|
string.Equals(e.Address, address, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| hosts.Intersect(e.SplittedHosts, StringComparer.InvariantCultureIgnoreCase).Any());
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
SetDuplicate(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDuplicate(Entry entry)
|
||||||
|
{
|
||||||
|
var hosts = entry.SplittedHosts;
|
||||||
|
|
||||||
|
entry.Duplicate = _entries.FirstOrDefault(e =>
|
||||||
|
e != entry
|
||||||
|
&& (string.Equals(e.Address, entry.Address, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| hosts.Intersect(e.SplittedHosts, StringComparer.InvariantCultureIgnoreCase).Any())) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObservableCollection<Entry> GetFilteredEntries()
|
||||||
|
{
|
||||||
|
if (_entries == null)
|
||||||
|
{
|
||||||
|
return new ObservableCollection<Entry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter = _entries.AsEnumerable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_addressFilter))
|
||||||
|
{
|
||||||
|
filter = filter.Where(e => e.Address.Contains(_addressFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_hostsFilter))
|
||||||
|
{
|
||||||
|
filter = filter.Where(e => e.Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_commentFilter))
|
||||||
|
{
|
||||||
|
filter = filter.Where(e => e.Comment.Contains(_commentFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showOnlyDuplicates)
|
||||||
|
{
|
||||||
|
filter = filter.Where(e => e.Duplicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ObservableCollection<Entry>(filter);
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|||||||
@@ -116,6 +116,9 @@
|
|||||||
</ic:EventTriggerBehavior>
|
</ic:EventTriggerBehavior>
|
||||||
</i:Interaction.Behaviors>
|
</i:Interaction.Behaviors>
|
||||||
</AutoSuggestBox>
|
</AutoSuggestBox>
|
||||||
|
<ToggleSwitch
|
||||||
|
x:Uid="ShowOnlyDuplicates"
|
||||||
|
IsOn="{x:Bind ViewModel.ShowOnlyDuplicates, Mode=TwoWay}" />
|
||||||
<Button
|
<Button
|
||||||
x:Uid="ClearFiltersBtn"
|
x:Uid="ClearFiltersBtn"
|
||||||
Margin="0,6,0,0"
|
Margin="0,6,0,0"
|
||||||
@@ -151,7 +154,6 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Orientation="Vertical"
|
Orientation="Vertical"
|
||||||
@@ -205,6 +207,7 @@
|
|||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<FlyoutBase.AttachedFlyout>
|
<FlyoutBase.AttachedFlyout>
|
||||||
<MenuFlyout>
|
<MenuFlyout>
|
||||||
@@ -254,6 +257,7 @@
|
|||||||
Margin="0,0,8,0"
|
Margin="0,0,8,0"
|
||||||
IsActive="{x:Bind Pinging, Mode=OneWay}" />
|
IsActive="{x:Bind Pinging, Mode=OneWay}" />
|
||||||
<FontIcon
|
<FontIcon
|
||||||
|
x:Uid="PingIcon"
|
||||||
x:Name="PingIcon"
|
x:Name="PingIcon"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="0,0,8,0"
|
Margin="0,0,8,0"
|
||||||
@@ -306,9 +310,18 @@
|
|||||||
</ic:DataTriggerBehavior>
|
</ic:DataTriggerBehavior>
|
||||||
</i:Interaction.Behaviors>
|
</i:Interaction.Behaviors>
|
||||||
</FontIcon>
|
</FontIcon>
|
||||||
|
<FontIcon
|
||||||
|
x:Uid="DuplicateEntryIcon"
|
||||||
|
Grid.Column="3"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Foreground="{StaticResource SystemControlErrorTextForegroundBrush}"
|
||||||
|
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="18"
|
||||||
|
Glyph=""
|
||||||
|
Visibility="{x:Bind Duplicate, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
x:Uid="ActiveToggle"
|
x:Uid="ActiveToggle"
|
||||||
Grid.Column="3"
|
Grid.Column="4"
|
||||||
Width="40"
|
Width="40"
|
||||||
MinWidth="0"
|
MinWidth="0"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
@@ -384,7 +397,6 @@
|
|||||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||||
ScrollViewer.VerticalScrollMode="Enabled"
|
ScrollViewer.VerticalScrollMode="Enabled"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
</ContentDialog>
|
</ContentDialog>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
Reference in New Issue
Block a user