mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-24 07:29:35 +01: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)]
|
||||
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.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)]
|
||||
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.Hosts, host);
|
||||
@@ -76,7 +76,7 @@ namespace Hosts.Tests
|
||||
[DataRow("host 10.1.1.1")]
|
||||
public void Not_Valid_Entry(string line)
|
||||
{
|
||||
var entry = new Entry(line);
|
||||
var entry = new Entry(0, line);
|
||||
Assert.IsFalse(entry.Valid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Hosts.Tests
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var entry = new Entry(line);
|
||||
var entry = new Entry(i, line);
|
||||
|
||||
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
|
||||
{
|
||||
SetProperty(ref _address, value);
|
||||
SetAddressType();
|
||||
OnPropertyChanged(nameof(Valid));
|
||||
}
|
||||
}
|
||||
@@ -54,22 +55,28 @@ namespace Hosts.Models
|
||||
[ObservableProperty]
|
||||
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 int Id { get; set; }
|
||||
|
||||
public Entry()
|
||||
{
|
||||
}
|
||||
|
||||
public Entry(string line)
|
||||
public Entry(int id, string line)
|
||||
{
|
||||
Id = id;
|
||||
_line = line.Trim();
|
||||
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();
|
||||
Hosts = hosts.Trim();
|
||||
Comment = comment.Trim();
|
||||
@@ -151,5 +158,21 @@ namespace Hosts.Models
|
||||
{
|
||||
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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Settings.UI.Library.Enumerations;
|
||||
|
||||
namespace Hosts.Settings
|
||||
@@ -10,6 +11,10 @@ namespace Hosts.Settings
|
||||
{
|
||||
public bool ShowStartupWarning { get; }
|
||||
|
||||
public bool LoopbackDuplicates { get; }
|
||||
|
||||
public AdditionalLinesPosition AdditionalLinesPosition { get; }
|
||||
|
||||
event EventHandler LoopbackDuplicatesChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,28 @@ namespace Hosts.Settings
|
||||
|
||||
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 UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
ShowStartupWarning = true;
|
||||
LoopbackDuplicates = false;
|
||||
AdditionalLinesPosition = AdditionalLinesPosition.Top;
|
||||
|
||||
LoadSettingsFromJson();
|
||||
@@ -35,6 +51,8 @@ namespace Hosts.Settings
|
||||
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
|
||||
}
|
||||
|
||||
public event EventHandler LoopbackDuplicatesChanged;
|
||||
|
||||
private void LoadSettingsFromJson()
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
@@ -60,6 +78,7 @@ namespace Hosts.Settings
|
||||
{
|
||||
ShowStartupWarning = settings.Properties.ShowStartupWarning;
|
||||
AdditionalLinesPosition = settings.Properties.AdditionalLinesPosition;
|
||||
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
|
||||
}
|
||||
|
||||
retry = false;
|
||||
|
||||
@@ -6,14 +6,17 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Common.UI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Hosts.Helpers;
|
||||
using Hosts.Models;
|
||||
using Hosts.Settings;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Hosts.ViewModels
|
||||
@@ -21,8 +24,18 @@ namespace Hosts.ViewModels
|
||||
public partial class MainViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IHostsService _hostsService;
|
||||
private readonly IUserSettings _userSettings;
|
||||
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 CancellationTokenSource _tokenSource;
|
||||
|
||||
[ObservableProperty]
|
||||
private Entry _selected;
|
||||
@@ -33,9 +46,6 @@ namespace Hosts.ViewModels
|
||||
[ObservableProperty]
|
||||
private bool _fileChanged;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _filtered;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _addressFilter;
|
||||
|
||||
@@ -45,38 +55,40 @@ namespace Hosts.ViewModels
|
||||
[ObservableProperty]
|
||||
private string _commentFilter;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Entries))]
|
||||
private bool _showOnlyDuplicates;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _additionalLines;
|
||||
|
||||
[ObservableProperty]
|
||||
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;
|
||||
|
||||
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 ICommand ClearFiltersCommand => new RelayCommand(ClearFilters);
|
||||
|
||||
public ICommand OpenSettingsCommand => new RelayCommand(OpenSettings);
|
||||
|
||||
public ICommand OpenHostsFileCommand => new RelayCommand(OpenHostsFile);
|
||||
|
||||
public MainViewModel(IHostsService hostService)
|
||||
public MainViewModel(IHostsService hostService, IUserSettings userSettings)
|
||||
{
|
||||
_hostsService = hostService;
|
||||
_userSettings = userSettings;
|
||||
|
||||
_hostsService.FileChanged += (s, e) =>
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
||||
};
|
||||
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
||||
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
|
||||
}
|
||||
|
||||
public void Add(Entry entry)
|
||||
@@ -85,12 +97,11 @@ namespace Hosts.ViewModels
|
||||
_entries.Add(entry);
|
||||
|
||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
}
|
||||
|
||||
public void Update(int index, Entry entry)
|
||||
{
|
||||
var existingEntry = Entries.ElementAt(index);
|
||||
var existingEntry = Entries[index] as Entry;
|
||||
var oldAddress = existingEntry.Address;
|
||||
var oldHosts = existingEntry.SplittedHosts;
|
||||
|
||||
@@ -101,7 +112,6 @@ namespace Hosts.ViewModels
|
||||
|
||||
FindDuplicates(oldAddress, oldHosts);
|
||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
}
|
||||
|
||||
public void DeleteSelected()
|
||||
@@ -111,7 +121,6 @@ namespace Hosts.ViewModels
|
||||
_entries.Remove(Selected);
|
||||
|
||||
FindDuplicates(address, hosts);
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (_readingHosts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
FileChanged = false;
|
||||
IsLoading = true;
|
||||
});
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
_readingHosts = true;
|
||||
(_additionalLines, var entries) = await _hostsService.ReadAsync();
|
||||
|
||||
await _dispatcherQueue.EnqueueAsync(() =>
|
||||
@@ -144,34 +179,66 @@ namespace Hosts.ViewModels
|
||||
}
|
||||
|
||||
_entries.CollectionChanged += Entries_CollectionChanged;
|
||||
Entries = new AdvancedCollectionView(_entries, true);
|
||||
Entries.SortDescriptions.Add(new SortDescription(nameof(Entry.Id), SortDirection.Ascending));
|
||||
ApplyFilters();
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
IsLoading = false;
|
||||
});
|
||||
_readingHosts = false;
|
||||
|
||||
FindDuplicates();
|
||||
_tokenSource?.Cancel();
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
FindDuplicates(_tokenSource.Token);
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
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)
|
||||
|| !string.IsNullOrWhiteSpace(_hostsFilter)
|
||||
|| !string.IsNullOrWhiteSpace(_commentFilter);
|
||||
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
if (!string.IsNullOrWhiteSpace(_hostsFilter))
|
||||
{
|
||||
expressions.Add(e => ((Entry)e).Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
AddressFilter = null;
|
||||
HostsFilter = null;
|
||||
CommentFilter = null;
|
||||
ShowOnlyDuplicates = false;
|
||||
Entries.Filter = null;
|
||||
Entries.RefreshFilter();
|
||||
}
|
||||
|
||||
public async Task PingSelectedAsync()
|
||||
@@ -183,11 +250,13 @@ namespace Hosts.ViewModels
|
||||
selected.Pinging = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenSettings()
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenHostsFile()
|
||||
{
|
||||
_hostsService.OpenHostsFile();
|
||||
@@ -201,6 +270,14 @@ namespace Hosts.ViewModels
|
||||
|
||||
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
|
||||
if (e.PropertyName == nameof(Entry.Ping)
|
||||
|| e.PropertyName == nameof(Entry.Pinging)
|
||||
@@ -225,12 +302,27 @@ namespace Hosts.ViewModels
|
||||
});
|
||||
}
|
||||
|
||||
private void FindDuplicates()
|
||||
private void FindDuplicates(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SetDuplicate(entry);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogInfo("FindDuplicates cancelled");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FindDuplicates(string address, IEnumerable<string> hosts)
|
||||
@@ -247,46 +339,27 @@ namespace Hosts.ViewModels
|
||||
|
||||
private void SetDuplicate(Entry entry)
|
||||
{
|
||||
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
entry.Duplicate = false;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var hosts = entry.SplittedHosts;
|
||||
|
||||
var duplicate = _entries.FirstOrDefault(e =>
|
||||
e != entry
|
||||
var duplicate = _entries.FirstOrDefault(e => e != entry
|
||||
&& e.Type == entry.Type
|
||||
&& (string.Equals(e.Address, entry.Address, StringComparison.InvariantCultureIgnoreCase)
|
||||
|| hosts.Intersect(e.SplittedHosts, StringComparer.InvariantCultureIgnoreCase).Any())) != null;
|
||||
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => entry.Duplicate = duplicate);
|
||||
}
|
||||
|
||||
private ObservableCollection<Entry> GetFilteredEntries()
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
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);
|
||||
entry.Duplicate = duplicate;
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Hosts.Views
|
||||
EntryDialog.Title = resourceLoader.GetString("AddNewEntryDialog_Title");
|
||||
EntryDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ namespace Hosts.Views
|
||||
var index = ViewModel.Entries.IndexOf(entry);
|
||||
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);
|
||||
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))]
|
||||
public bool LaunchAdministrator { get; set; }
|
||||
|
||||
[JsonConverter(typeof(BoolPropertyJsonConverter))]
|
||||
public bool LoopbackDuplicates { get; set; }
|
||||
|
||||
public AdditionalLinesPosition AdditionalLinesPosition { get; set; }
|
||||
|
||||
public HostsProperties()
|
||||
{
|
||||
ShowStartupWarning = true;
|
||||
LaunchAdministrator = true;
|
||||
LoopbackDuplicates = false;
|
||||
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">
|
||||
<value>Top</value>
|
||||
</data>
|
||||
<data name="Hosts_File_GroupSettings.Header" xml:space="preserve">
|
||||
<value>File</value>
|
||||
<data name="Hosts_Behavior_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Behavior</value>
|
||||
</data>
|
||||
<data name="Launch_Hosts.Content" xml:space="preserve">
|
||||
<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">
|
||||
<value>The system administrator has disabled the automatic download of updates.</value>
|
||||
</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>
|
||||
@@ -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
|
||||
{
|
||||
get => Settings.Properties.LaunchAdministrator;
|
||||
|
||||
@@ -46,13 +46,20 @@
|
||||
</labs:SettingsCard>
|
||||
</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=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.AdditionalLinesPosition, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Top" />
|
||||
<ComboBoxItem x:Uid="Hosts_AdditionalLinesPosition_Bottom" />
|
||||
</ComboBox>
|
||||
</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>
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
|
||||
Reference in New Issue
Block a user