mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-08 04:07:40 +02:00
[Hosts] Improved duplicate hosts finding (#24294)
* improved duplicate hosts finding * improved filters with AdvancedCollectionView * cancel FindDuplicates when file is reloaded
This commit is contained in:
committed by
GitHub
parent
0d9b797ef0
commit
3e651b8de6
@@ -28,7 +28,7 @@ namespace Hosts.Tests
|
|||||||
[DataRow("# # \t10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
|
[DataRow("# # \t10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
|
||||||
public void Valid_Entry_SingleHost(string line, string address, string host, string comment, bool active)
|
public void Valid_Entry_SingleHost(string line, string address, string host, string comment, bool active)
|
||||||
{
|
{
|
||||||
var entry = new Entry(line);
|
var entry = new Entry(0, line);
|
||||||
|
|
||||||
Assert.AreEqual(entry.Address, address);
|
Assert.AreEqual(entry.Address, address);
|
||||||
Assert.AreEqual(entry.Hosts, host);
|
Assert.AreEqual(entry.Hosts, host);
|
||||||
@@ -52,7 +52,7 @@ namespace Hosts.Tests
|
|||||||
[DataRow("#10.1.1.1 host host.local#comment", "10.1.1.1", "host host.local", "comment", false)]
|
[DataRow("#10.1.1.1 host host.local#comment", "10.1.1.1", "host host.local", "comment", false)]
|
||||||
public void Valid_Entry_MultipleHosts(string line, string address, string host, string comment, bool active)
|
public void Valid_Entry_MultipleHosts(string line, string address, string host, string comment, bool active)
|
||||||
{
|
{
|
||||||
var entry = new Entry(line);
|
var entry = new Entry(0, line);
|
||||||
|
|
||||||
Assert.AreEqual(entry.Address, address);
|
Assert.AreEqual(entry.Address, address);
|
||||||
Assert.AreEqual(entry.Hosts, host);
|
Assert.AreEqual(entry.Hosts, host);
|
||||||
@@ -76,7 +76,7 @@ namespace Hosts.Tests
|
|||||||
[DataRow("host 10.1.1.1")]
|
[DataRow("host 10.1.1.1")]
|
||||||
public void Not_Valid_Entry(string line)
|
public void Not_Valid_Entry(string line)
|
||||||
{
|
{
|
||||||
var entry = new Entry(line);
|
var entry = new Entry(0, line);
|
||||||
Assert.IsFalse(entry.Valid);
|
Assert.IsFalse(entry.Valid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ namespace Hosts.Tests
|
|||||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||||
|
|
||||||
var (_, entries) = await service.ReadAsync();
|
var (_, entries) = await service.ReadAsync();
|
||||||
entries.Add(new Entry("10.1.1.30", "host30 host30.local", "new entry", false));
|
entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
|
||||||
await service.WriteAsync(string.Empty, entries);
|
await service.WriteAsync(string.Empty, entries);
|
||||||
|
|
||||||
var result = fileSystem.GetFile(service.HostsFilePath);
|
var result = fileSystem.GetFile(service.HostsFilePath);
|
||||||
|
|||||||
43
src/modules/Hosts/Hosts/Helpers/ExpressionExtensions.cs
Normal file
43
src/modules/Hosts/Hosts/Helpers/ExpressionExtensions.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace Hosts.Helpers
|
||||||
|
{
|
||||||
|
// https://stackoverflow.com/a/22569086
|
||||||
|
public static class ExpressionExtensions
|
||||||
|
{
|
||||||
|
public static Expression<Func<T, bool>> And<T>(
|
||||||
|
this Expression<Func<T, bool>> expr1,
|
||||||
|
Expression<Func<T, bool>> expr2)
|
||||||
|
{
|
||||||
|
var secondBody = expr2.Body.Replace(expr2.Parameters[0], expr1.Parameters[0]);
|
||||||
|
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression Replace(this Expression expression, Expression searchEx, Expression replaceEx)
|
||||||
|
{
|
||||||
|
return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ReplaceVisitor : ExpressionVisitor
|
||||||
|
{
|
||||||
|
private readonly Expression _from;
|
||||||
|
private readonly Expression _to;
|
||||||
|
|
||||||
|
public ReplaceVisitor(Expression from, Expression to)
|
||||||
|
{
|
||||||
|
_from = from;
|
||||||
|
_to = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Expression Visit(Expression node)
|
||||||
|
{
|
||||||
|
return node == _from ? _to : base.Visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ namespace Hosts.Helpers
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = new Entry(line);
|
var entry = new Entry(i, line);
|
||||||
|
|
||||||
if (entry.Valid)
|
if (entry.Valid)
|
||||||
{
|
{
|
||||||
|
|||||||
13
src/modules/Hosts/Hosts/Models/AddressType.cs
Normal file
13
src/modules/Hosts/Hosts/Models/AddressType.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Hosts.Models
|
||||||
|
{
|
||||||
|
public enum AddressType
|
||||||
|
{
|
||||||
|
Invalid = 0,
|
||||||
|
IPv4 = 1,
|
||||||
|
IPv6 = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ namespace Hosts.Models
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
SetProperty(ref _address, value);
|
SetProperty(ref _address, value);
|
||||||
|
SetAddressType();
|
||||||
OnPropertyChanged(nameof(Valid));
|
OnPropertyChanged(nameof(Valid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,22 +55,28 @@ namespace Hosts.Models
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _duplicate;
|
private bool _duplicate;
|
||||||
|
|
||||||
public bool Valid => ValidationHelper.ValidHosts(_hosts) && (ValidationHelper.ValidIPv4(_address) || ValidationHelper.ValidIPv6(_address));
|
public bool Valid => ValidationHelper.ValidHosts(_hosts) && Type != AddressType.Invalid;
|
||||||
|
|
||||||
|
public AddressType Type { get; private set; }
|
||||||
|
|
||||||
public string[] SplittedHosts { get; private set; }
|
public string[] SplittedHosts { get; private set; }
|
||||||
|
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
public Entry()
|
public Entry()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry(string line)
|
public Entry(int id, string line)
|
||||||
{
|
{
|
||||||
|
Id = id;
|
||||||
_line = line.Trim();
|
_line = line.Trim();
|
||||||
Parse();
|
Parse();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry(string address, string hosts, string comment, bool active)
|
public Entry(int id, string address, string hosts, string comment, bool active)
|
||||||
{
|
{
|
||||||
|
Id = id;
|
||||||
Address = address.Trim();
|
Address = address.Trim();
|
||||||
Hosts = hosts.Trim();
|
Hosts = hosts.Trim();
|
||||||
Comment = comment.Trim();
|
Comment = comment.Trim();
|
||||||
@@ -151,5 +158,21 @@ namespace Hosts.Models
|
|||||||
{
|
{
|
||||||
return _line;
|
return _line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetAddressType()
|
||||||
|
{
|
||||||
|
if (ValidationHelper.ValidIPv4(_address))
|
||||||
|
{
|
||||||
|
Type = AddressType.IPv4;
|
||||||
|
}
|
||||||
|
else if (ValidationHelper.ValidIPv6(_address))
|
||||||
|
{
|
||||||
|
Type = AddressType.IPv6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Type = AddressType.Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// 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 Settings.UI.Library.Enumerations;
|
using Settings.UI.Library.Enumerations;
|
||||||
|
|
||||||
namespace Hosts.Settings
|
namespace Hosts.Settings
|
||||||
@@ -10,6 +11,10 @@ namespace Hosts.Settings
|
|||||||
{
|
{
|
||||||
public bool ShowStartupWarning { get; }
|
public bool ShowStartupWarning { get; }
|
||||||
|
|
||||||
|
public bool LoopbackDuplicates { get; }
|
||||||
|
|
||||||
public AdditionalLinesPosition AdditionalLinesPosition { get; }
|
public AdditionalLinesPosition AdditionalLinesPosition { get; }
|
||||||
|
|
||||||
|
event EventHandler LoopbackDuplicatesChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,28 @@ namespace Hosts.Settings
|
|||||||
|
|
||||||
public bool ShowStartupWarning { get; private set; }
|
public bool ShowStartupWarning { get; private set; }
|
||||||
|
|
||||||
|
private bool _loopbackDuplicates;
|
||||||
|
|
||||||
|
public bool LoopbackDuplicates
|
||||||
|
{
|
||||||
|
get => _loopbackDuplicates;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_loopbackDuplicates != value)
|
||||||
|
{
|
||||||
|
_loopbackDuplicates = value;
|
||||||
|
LoopbackDuplicatesChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public AdditionalLinesPosition AdditionalLinesPosition { get; private set; }
|
public AdditionalLinesPosition AdditionalLinesPosition { get; private set; }
|
||||||
|
|
||||||
public UserSettings()
|
public UserSettings()
|
||||||
{
|
{
|
||||||
_settingsUtils = new SettingsUtils();
|
_settingsUtils = new SettingsUtils();
|
||||||
ShowStartupWarning = true;
|
ShowStartupWarning = true;
|
||||||
|
LoopbackDuplicates = false;
|
||||||
AdditionalLinesPosition = AdditionalLinesPosition.Top;
|
AdditionalLinesPosition = AdditionalLinesPosition.Top;
|
||||||
|
|
||||||
LoadSettingsFromJson();
|
LoadSettingsFromJson();
|
||||||
@@ -35,6 +51,8 @@ namespace Hosts.Settings
|
|||||||
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
|
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public event EventHandler LoopbackDuplicatesChanged;
|
||||||
|
|
||||||
private void LoadSettingsFromJson()
|
private void LoadSettingsFromJson()
|
||||||
{
|
{
|
||||||
lock (_loadingSettingsLock)
|
lock (_loadingSettingsLock)
|
||||||
@@ -60,6 +78,7 @@ namespace Hosts.Settings
|
|||||||
{
|
{
|
||||||
ShowStartupWarning = settings.Properties.ShowStartupWarning;
|
ShowStartupWarning = settings.Properties.ShowStartupWarning;
|
||||||
AdditionalLinesPosition = settings.Properties.AdditionalLinesPosition;
|
AdditionalLinesPosition = settings.Properties.AdditionalLinesPosition;
|
||||||
|
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
retry = false;
|
retry = false;
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using Common.UI;
|
using Common.UI;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
|
using CommunityToolkit.WinUI.UI;
|
||||||
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,8 +24,18 @@ 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 readonly string[] _loopbackAddresses =
|
||||||
|
{
|
||||||
|
"127.0.0.1",
|
||||||
|
"::1",
|
||||||
|
"0:0:0:0:0:0:0:1",
|
||||||
|
};
|
||||||
|
|
||||||
|
private bool _readingHosts;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
private CancellationTokenSource _tokenSource;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private Entry _selected;
|
private Entry _selected;
|
||||||
@@ -33,9 +46,6 @@ namespace Hosts.ViewModels
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _fileChanged;
|
private bool _fileChanged;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _filtered;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _addressFilter;
|
private string _addressFilter;
|
||||||
|
|
||||||
@@ -45,38 +55,40 @@ namespace Hosts.ViewModels
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _commentFilter;
|
private string _commentFilter;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(Entries))]
|
|
||||||
private bool _showOnlyDuplicates;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _additionalLines;
|
private string _additionalLines;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _filtered;
|
||||||
|
|
||||||
|
private bool _showOnlyDuplicates;
|
||||||
|
|
||||||
|
public bool ShowOnlyDuplicates
|
||||||
|
{
|
||||||
|
get => _showOnlyDuplicates;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _showOnlyDuplicates, value);
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ObservableCollection<Entry> _entries;
|
private ObservableCollection<Entry> _entries;
|
||||||
|
|
||||||
public ObservableCollection<Entry> Entries => _filtered || _showOnlyDuplicates ? GetFilteredEntries() : _entries;
|
public AdvancedCollectionView Entries { get; set; }
|
||||||
|
|
||||||
public ICommand ReadHostsCommand => new RelayCommand(ReadHosts);
|
public int NextId => _entries.Max(e => e.Id) + 1;
|
||||||
|
|
||||||
public ICommand ApplyFiltersCommand => new RelayCommand(ApplyFilters);
|
public MainViewModel(IHostsService hostService, IUserSettings userSettings)
|
||||||
|
|
||||||
public ICommand ClearFiltersCommand => new RelayCommand(ClearFilters);
|
|
||||||
|
|
||||||
public ICommand OpenSettingsCommand => new RelayCommand(OpenSettings);
|
|
||||||
|
|
||||||
public ICommand OpenHostsFileCommand => new RelayCommand(OpenHostsFile);
|
|
||||||
|
|
||||||
public MainViewModel(IHostsService hostService)
|
|
||||||
{
|
{
|
||||||
_hostsService = hostService;
|
_hostsService = hostService;
|
||||||
|
_userSettings = userSettings;
|
||||||
|
|
||||||
_hostsService.FileChanged += (s, e) =>
|
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
||||||
{
|
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
|
||||||
_dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(Entry entry)
|
public void Add(Entry entry)
|
||||||
@@ -85,12 +97,11 @@ namespace Hosts.ViewModels
|
|||||||
_entries.Add(entry);
|
_entries.Add(entry);
|
||||||
|
|
||||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
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[index] as Entry;
|
||||||
var oldAddress = existingEntry.Address;
|
var oldAddress = existingEntry.Address;
|
||||||
var oldHosts = existingEntry.SplittedHosts;
|
var oldHosts = existingEntry.SplittedHosts;
|
||||||
|
|
||||||
@@ -101,7 +112,6 @@ namespace Hosts.ViewModels
|
|||||||
|
|
||||||
FindDuplicates(oldAddress, oldHosts);
|
FindDuplicates(oldAddress, oldHosts);
|
||||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||||
OnPropertyChanged(nameof(Entries));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteSelected()
|
public void DeleteSelected()
|
||||||
@@ -111,7 +121,6 @@ namespace Hosts.ViewModels
|
|||||||
_entries.Remove(Selected);
|
_entries.Remove(Selected);
|
||||||
|
|
||||||
FindDuplicates(address, hosts);
|
FindDuplicates(address, hosts);
|
||||||
OnPropertyChanged(nameof(Entries));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateAdditionalLines(string lines)
|
public void UpdateAdditionalLines(string lines)
|
||||||
@@ -125,13 +134,39 @@ namespace Hosts.ViewModels
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Move(int oldIndex, int newIndex)
|
||||||
|
{
|
||||||
|
if (Filtered)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap the IDs
|
||||||
|
var entry1 = _entries[oldIndex];
|
||||||
|
var entry2 = _entries[newIndex];
|
||||||
|
(entry2.Id, entry1.Id) = (entry1.Id, entry2.Id);
|
||||||
|
|
||||||
|
// Move entries in the UI
|
||||||
|
_entries.Move(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
public void ReadHosts()
|
public void ReadHosts()
|
||||||
{
|
{
|
||||||
FileChanged = false;
|
if (_readingHosts)
|
||||||
IsLoading = true;
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
FileChanged = false;
|
||||||
|
IsLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
_readingHosts = true;
|
||||||
(_additionalLines, var entries) = await _hostsService.ReadAsync();
|
(_additionalLines, var entries) = await _hostsService.ReadAsync();
|
||||||
|
|
||||||
await _dispatcherQueue.EnqueueAsync(() =>
|
await _dispatcherQueue.EnqueueAsync(() =>
|
||||||
@@ -144,34 +179,66 @@ namespace Hosts.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
_entries.CollectionChanged += Entries_CollectionChanged;
|
_entries.CollectionChanged += Entries_CollectionChanged;
|
||||||
|
Entries = new AdvancedCollectionView(_entries, true);
|
||||||
|
Entries.SortDescriptions.Add(new SortDescription(nameof(Entry.Id), SortDirection.Ascending));
|
||||||
|
ApplyFilters();
|
||||||
OnPropertyChanged(nameof(Entries));
|
OnPropertyChanged(nameof(Entries));
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
});
|
});
|
||||||
|
_readingHosts = false;
|
||||||
|
|
||||||
FindDuplicates();
|
_tokenSource?.Cancel();
|
||||||
|
_tokenSource = new CancellationTokenSource();
|
||||||
|
FindDuplicates(_tokenSource.Token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
public void ApplyFilters()
|
public void ApplyFilters()
|
||||||
{
|
{
|
||||||
if (_entries == null)
|
var expressions = new List<Expression<Func<object, bool>>>(4);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_addressFilter))
|
||||||
{
|
{
|
||||||
return;
|
expressions.Add(e => ((Entry)e).Address.Contains(_addressFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
Filtered = !string.IsNullOrWhiteSpace(_addressFilter)
|
if (!string.IsNullOrWhiteSpace(_hostsFilter))
|
||||||
|| !string.IsNullOrWhiteSpace(_hostsFilter)
|
{
|
||||||
|| !string.IsNullOrWhiteSpace(_commentFilter);
|
expressions.Add(e => ((Entry)e).Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
OnPropertyChanged(nameof(Entries));
|
if (!string.IsNullOrWhiteSpace(_commentFilter))
|
||||||
|
{
|
||||||
|
expressions.Add(e => ((Entry)e).Comment.Contains(_commentFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showOnlyDuplicates)
|
||||||
|
{
|
||||||
|
expressions.Add(e => ((Entry)e).Duplicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression<Func<object, bool>> filterExpression = null;
|
||||||
|
|
||||||
|
foreach (var e in expressions)
|
||||||
|
{
|
||||||
|
filterExpression = filterExpression == null ? e : filterExpression.And(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Filtered = filterExpression != null;
|
||||||
|
Entries.Filter = Filtered ? filterExpression.Compile().Invoke : null;
|
||||||
|
Entries.RefreshFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
public void ClearFilters()
|
public void ClearFilters()
|
||||||
{
|
{
|
||||||
AddressFilter = null;
|
AddressFilter = null;
|
||||||
HostsFilter = null;
|
HostsFilter = null;
|
||||||
CommentFilter = null;
|
CommentFilter = null;
|
||||||
ShowOnlyDuplicates = false;
|
ShowOnlyDuplicates = false;
|
||||||
|
Entries.Filter = null;
|
||||||
|
Entries.RefreshFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PingSelectedAsync()
|
public async Task PingSelectedAsync()
|
||||||
@@ -183,11 +250,13 @@ namespace Hosts.ViewModels
|
|||||||
selected.Pinging = false;
|
selected.Pinging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
public void OpenSettings()
|
public void OpenSettings()
|
||||||
{
|
{
|
||||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts);
|
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
public void OpenHostsFile()
|
public void OpenHostsFile()
|
||||||
{
|
{
|
||||||
_hostsService.OpenHostsFile();
|
_hostsService.OpenHostsFile();
|
||||||
@@ -201,6 +270,14 @@ namespace Hosts.ViewModels
|
|||||||
|
|
||||||
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (Filtered && (e.PropertyName == nameof(Entry.Hosts)
|
||||||
|
|| e.PropertyName == nameof(Entry.Address)
|
||||||
|
|| e.PropertyName == nameof(Entry.Comment)
|
||||||
|
|| e.PropertyName == nameof(Entry.Duplicate)))
|
||||||
|
{
|
||||||
|
Entries.RefreshFilter();
|
||||||
|
}
|
||||||
|
|
||||||
// Ping and duplicate should't trigger a file save
|
// Ping and duplicate should't trigger a file save
|
||||||
if (e.PropertyName == nameof(Entry.Ping)
|
if (e.PropertyName == nameof(Entry.Ping)
|
||||||
|| e.PropertyName == nameof(Entry.Pinging)
|
|| e.PropertyName == nameof(Entry.Pinging)
|
||||||
@@ -225,11 +302,26 @@ namespace Hosts.ViewModels
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FindDuplicates()
|
private void FindDuplicates(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
foreach (var entry in _entries)
|
foreach (var entry in _entries)
|
||||||
{
|
{
|
||||||
SetDuplicate(entry);
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetDuplicate(entry);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogInfo("FindDuplicates cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,46 +339,27 @@ namespace Hosts.ViewModels
|
|||||||
|
|
||||||
private void SetDuplicate(Entry entry)
|
private void SetDuplicate(Entry entry)
|
||||||
{
|
{
|
||||||
|
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
|
||||||
|
{
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
entry.Duplicate = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var hosts = entry.SplittedHosts;
|
var hosts = entry.SplittedHosts;
|
||||||
|
|
||||||
var duplicate = _entries.FirstOrDefault(e =>
|
var duplicate = _entries.FirstOrDefault(e => e != entry
|
||||||
e != entry
|
&& e.Type == entry.Type
|
||||||
&& (string.Equals(e.Address, entry.Address, StringComparison.InvariantCultureIgnoreCase)
|
&& (string.Equals(e.Address, entry.Address, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|| hosts.Intersect(e.SplittedHosts, StringComparer.InvariantCultureIgnoreCase).Any())) != null;
|
|| hosts.Intersect(e.SplittedHosts, StringComparer.InvariantCultureIgnoreCase).Any())) != null;
|
||||||
|
|
||||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => entry.Duplicate = duplicate);
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
}
|
|
||||||
|
|
||||||
private ObservableCollection<Entry> GetFilteredEntries()
|
|
||||||
{
|
|
||||||
if (_entries == null)
|
|
||||||
{
|
{
|
||||||
return new ObservableCollection<Entry>();
|
entry.Duplicate = duplicate;
|
||||||
}
|
});
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ namespace Hosts.Views
|
|||||||
EntryDialog.Title = resourceLoader.GetString("AddNewEntryDialog_Title");
|
EntryDialog.Title = resourceLoader.GetString("AddNewEntryDialog_Title");
|
||||||
EntryDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
|
EntryDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
|
||||||
EntryDialog.PrimaryButtonCommand = AddCommand;
|
EntryDialog.PrimaryButtonCommand = AddCommand;
|
||||||
EntryDialog.DataContext = new Entry(string.Empty, string.Empty, string.Empty, true);
|
EntryDialog.DataContext = new Entry(ViewModel.NextId, string.Empty, string.Empty, string.Empty, true);
|
||||||
await EntryDialog.ShowAsync();
|
await EntryDialog.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ namespace Hosts.Views
|
|||||||
var index = ViewModel.Entries.IndexOf(entry);
|
var index = ViewModel.Entries.IndexOf(entry);
|
||||||
if (index > 0)
|
if (index > 0)
|
||||||
{
|
{
|
||||||
ViewModel.Entries.Move(index, index - 1);
|
ViewModel.Move(index, index - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ namespace Hosts.Views
|
|||||||
var index = ViewModel.Entries.IndexOf(entry);
|
var index = ViewModel.Entries.IndexOf(entry);
|
||||||
if (index < ViewModel.Entries.Count - 1)
|
if (index < ViewModel.Entries.Count - 1)
|
||||||
{
|
{
|
||||||
ViewModel.Entries.Move(index, index + 1);
|
ViewModel.Move(index, index + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
[JsonConverter(typeof(BoolPropertyJsonConverter))]
|
[JsonConverter(typeof(BoolPropertyJsonConverter))]
|
||||||
public bool LaunchAdministrator { get; set; }
|
public bool LaunchAdministrator { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(BoolPropertyJsonConverter))]
|
||||||
|
public bool LoopbackDuplicates { get; set; }
|
||||||
|
|
||||||
public AdditionalLinesPosition AdditionalLinesPosition { get; set; }
|
public AdditionalLinesPosition AdditionalLinesPosition { get; set; }
|
||||||
|
|
||||||
public HostsProperties()
|
public HostsProperties()
|
||||||
{
|
{
|
||||||
ShowStartupWarning = true;
|
ShowStartupWarning = true;
|
||||||
LaunchAdministrator = true;
|
LaunchAdministrator = true;
|
||||||
|
LoopbackDuplicates = false;
|
||||||
AdditionalLinesPosition = AdditionalLinesPosition.Top;
|
AdditionalLinesPosition = AdditionalLinesPosition.Top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2855,8 +2855,8 @@ Activate by holding the key for the character you want to add an accent to, then
|
|||||||
<data name="Hosts_AdditionalLinesPosition_Top.Content" xml:space="preserve">
|
<data name="Hosts_AdditionalLinesPosition_Top.Content" xml:space="preserve">
|
||||||
<value>Top</value>
|
<value>Top</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Hosts_File_GroupSettings.Header" xml:space="preserve">
|
<data name="Hosts_Behavior_GroupSettings.Header" xml:space="preserve">
|
||||||
<value>File</value>
|
<value>Behavior</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Launch_Hosts.Content" xml:space="preserve">
|
<data name="Launch_Hosts.Content" xml:space="preserve">
|
||||||
<value>Launch Hosts File Editor</value>
|
<value>Launch Hosts File Editor</value>
|
||||||
@@ -3043,4 +3043,11 @@ Activate by holding the key for the character you want to add an accent to, then
|
|||||||
<data name="GPO_AutoDownloadUpdatesIsDisabled.Title" xml:space="preserve">
|
<data name="GPO_AutoDownloadUpdatesIsDisabled.Title" xml:space="preserve">
|
||||||
<value>The system administrator has disabled the automatic download of updates.</value>
|
<value>The system administrator has disabled the automatic download of updates.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Hosts_Toggle_LoopbackDuplicates.Description" xml:space="preserve">
|
||||||
|
<value>127.0.0.1, ::1, ...</value>
|
||||||
|
<comment>"127.0.0.1 and ::1" are well known loopback addresses, do not loc</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Hosts_Toggle_LoopbackDuplicates.Header" xml:space="preserve">
|
||||||
|
<value>Consider loopback addresses as duplicates</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -76,6 +76,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool LoopbackDuplicates
|
||||||
|
{
|
||||||
|
get => Settings.Properties.LoopbackDuplicates;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != Settings.Properties.LoopbackDuplicates)
|
||||||
|
{
|
||||||
|
Settings.Properties.LoopbackDuplicates = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool LaunchAdministrator
|
public bool LaunchAdministrator
|
||||||
{
|
{
|
||||||
get => Settings.Properties.LaunchAdministrator;
|
get => Settings.Properties.LaunchAdministrator;
|
||||||
|
|||||||
@@ -46,13 +46,20 @@
|
|||||||
</labs:SettingsCard>
|
</labs:SettingsCard>
|
||||||
</controls:SettingsGroup>
|
</controls:SettingsGroup>
|
||||||
|
|
||||||
<controls:SettingsGroup x:Uid="Hosts_File_GroupSettings" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
|
<controls:SettingsGroup x:Uid="Hosts_Behavior_GroupSettings" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
|
||||||
<labs:SettingsCard x:Uid="Hosts_AdditionalLinesPosition" HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=}">
|
<labs:SettingsCard x:Uid="Hosts_AdditionalLinesPosition" HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=}">
|
||||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.AdditionalLinesPosition, Mode=TwoWay}">
|
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.AdditionalLinesPosition, Mode=TwoWay}">
|
||||||
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Top" />
|
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Top" />
|
||||||
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Bottom" />
|
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Bottom" />
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</labs:SettingsCard>
|
</labs:SettingsCard>
|
||||||
|
<labs:SettingsCard
|
||||||
|
x:Uid="Hosts_Toggle_LoopbackDuplicates"
|
||||||
|
HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=}">
|
||||||
|
<ToggleSwitch
|
||||||
|
x:Uid="ToggleSwitch"
|
||||||
|
IsOn="{x:Bind Mode=TwoWay, Path=ViewModel.LoopbackDuplicates}" />
|
||||||
|
</labs:SettingsCard>
|
||||||
</controls:SettingsGroup>
|
</controls:SettingsGroup>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsPageControl.ModuleContent>
|
</controls:SettingsPageControl.ModuleContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user