mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 19:26:39 +02:00
[EnvVar][Hosts][RegPrev]Decouple and refactor to make it "packable" as nuget package (#32604)
* WIP Hosts - remove deps * Add consumer app * Move App and MainWindow to Consumer app. Make Hosts dll * Try consume it * Fix errors * Make it work with custom build targets * Dependency injection Refactor Explicit page creation Wire missing dependencies * Fix installer * Remove unneeded stuff * Fix build again * Extract UI and logic from MainWindow to RegistryPreviewMainPage * Convert to lib Change namespace to RegistryPreviewUILib Remove PT deps * Add exe app and move App.xaml and MainWindow.xaml * Consume the lib * Update Hosts package creation * Fix RegistryPreview package creation * Rename RegistryPreviewUI back to RegistryPreview * Back to consuming lib * Ship icons and assets in nuget packages * Rename to EnvironmentVariablesUILib and convert to lib * Add app and consume * Telemetry * GPO * nuget * Rename HostsPackageConsumer to Hosts and Hosts lib to HostsUILib * Assets cleanup * nuget struct * v0 * assets * [Hosts] Re-add AppList to Lib Assets, [RegPrev] Copy lib assets to out dir * Sign UI dlls * Revert WinUIEx bump * Cleanup * Align deps * version exception dll * Fix RegistryPreview crashes * XAML format * XAML format 2 * Pack .pri files in lib/ dir --------- Co-authored-by: Darshak Bhatti <dabhatti@microsoft.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 567 B |
Binary file not shown.
|
After Width: | Height: | Size: 768 B |
Binary file not shown.
|
After Width: | Height: | Size: 1001 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/modules/Hosts/HostsUILib/Assets/HostsUILib/Hosts.ico
Normal file
BIN
src/modules/Hosts/HostsUILib/Assets/HostsUILib/Hosts.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
14
src/modules/Hosts/HostsUILib/Consts.cs
Normal file
14
src/modules/Hosts/HostsUILib/Consts.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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 HostsUILib
|
||||
{
|
||||
public static class Consts
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of hosts detected by the system in a single line
|
||||
/// </summary>
|
||||
public const int MaxHostsCount = 9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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;
|
||||
|
||||
namespace HostsUILib.Exceptions
|
||||
{
|
||||
public class NotRunningElevatedException : Exception
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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;
|
||||
|
||||
namespace HostsUILib.Exceptions
|
||||
{
|
||||
public class ReadOnlyHostsException : Exception
|
||||
{
|
||||
}
|
||||
}
|
||||
20
src/modules/Hosts/HostsUILib/Helpers/ElevationHelper.cs
Normal file
20
src/modules/Hosts/HostsUILib/Helpers/ElevationHelper.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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.Security.Principal;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
public class ElevationHelper : IElevationHelper
|
||||
{
|
||||
private readonly bool _isElevated;
|
||||
|
||||
public bool IsElevated => _isElevated;
|
||||
|
||||
public ElevationHelper()
|
||||
{
|
||||
_isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/modules/Hosts/HostsUILib/Helpers/ExpressionExtensions.cs
Normal file
43
src/modules/Hosts/HostsUILib/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 HostsUILib.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 sealed 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
329
src/modules/Hosts/HostsUILib/Helpers/HostsService.cs
Normal file
329
src/modules/Hosts/HostsUILib/Helpers/HostsService.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using HostsUILib.Exceptions;
|
||||
using HostsUILib.Models;
|
||||
using HostsUILib.Settings;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
public class HostsService : IHostsService, IDisposable
|
||||
{
|
||||
private const string _backupSuffix = $"_PowerToysBackup_";
|
||||
|
||||
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly IElevationHelper _elevationHelper;
|
||||
private readonly IFileSystemWatcher _fileSystemWatcher;
|
||||
private readonly string _hostsFilePath;
|
||||
private bool _backupDone;
|
||||
private bool _disposed;
|
||||
|
||||
public string HostsFilePath => _hostsFilePath;
|
||||
|
||||
public event EventHandler FileChanged;
|
||||
|
||||
public Encoding Encoding => _userSettings.Encoding == HostsEncoding.Utf8 ? new UTF8Encoding(false) : new UTF8Encoding(true);
|
||||
|
||||
public HostsService(
|
||||
IFileSystem fileSystem,
|
||||
IUserSettings userSettings,
|
||||
IElevationHelper elevationHelper)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_userSettings = userSettings;
|
||||
_elevationHelper = elevationHelper;
|
||||
|
||||
_hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts");
|
||||
|
||||
_fileSystemWatcher = _fileSystem.FileSystemWatcher.CreateNew();
|
||||
_fileSystemWatcher.Path = _fileSystem.Path.GetDirectoryName(HostsFilePath);
|
||||
_fileSystemWatcher.Filter = _fileSystem.Path.GetFileName(HostsFilePath);
|
||||
_fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite;
|
||||
_fileSystemWatcher.Changed += FileSystemWatcher_Changed;
|
||||
_fileSystemWatcher.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
public bool Exists()
|
||||
{
|
||||
return _fileSystem.File.Exists(HostsFilePath);
|
||||
}
|
||||
|
||||
public async Task<HostsData> ReadAsync()
|
||||
{
|
||||
var entries = new List<Entry>();
|
||||
var unparsedBuilder = new StringBuilder();
|
||||
var splittedEntries = false;
|
||||
|
||||
if (!Exists())
|
||||
{
|
||||
return new HostsData(entries, unparsedBuilder.ToString(), false);
|
||||
}
|
||||
|
||||
var lines = await _fileSystem.File.ReadAllLinesAsync(HostsFilePath, Encoding);
|
||||
|
||||
var id = 0;
|
||||
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = new Entry(id, line);
|
||||
|
||||
if (entry.Valid)
|
||||
{
|
||||
entries.Add(entry);
|
||||
id++;
|
||||
}
|
||||
else if (entry.Validate(false))
|
||||
{
|
||||
foreach (var hostsChunk in entry.SplittedHosts.Chunk(Consts.MaxHostsCount))
|
||||
{
|
||||
var clonedEntry = entry.Clone();
|
||||
clonedEntry.Id = id;
|
||||
clonedEntry.Hosts = string.Join(' ', hostsChunk);
|
||||
entries.Add(clonedEntry);
|
||||
id++;
|
||||
}
|
||||
|
||||
splittedEntries = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (unparsedBuilder.Length > 0)
|
||||
{
|
||||
unparsedBuilder.Append(Environment.NewLine);
|
||||
}
|
||||
|
||||
unparsedBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
|
||||
return new HostsData(entries, unparsedBuilder.ToString(), splittedEntries);
|
||||
}
|
||||
|
||||
public async Task WriteAsync(string additionalLines, IEnumerable<Entry> entries)
|
||||
{
|
||||
if (!_elevationHelper.IsElevated)
|
||||
{
|
||||
throw new NotRunningElevatedException();
|
||||
}
|
||||
|
||||
if (_fileSystem.FileInfo.FromFileName(HostsFilePath).IsReadOnly)
|
||||
{
|
||||
throw new ReadOnlyHostsException();
|
||||
}
|
||||
|
||||
var lines = new List<string>();
|
||||
|
||||
if (entries.Any())
|
||||
{
|
||||
var addressPadding = entries.Max(e => e.Address.Length) + 1;
|
||||
var hostsPadding = entries.Max(e => e.Hosts.Length) + 1;
|
||||
var anyDisabled = entries.Any(e => !e.Active);
|
||||
|
||||
foreach (var e in entries)
|
||||
{
|
||||
var lineBuilder = new StringBuilder();
|
||||
|
||||
if (!e.Valid)
|
||||
{
|
||||
lineBuilder.Append(e.Line);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!e.Active)
|
||||
{
|
||||
lineBuilder.Append('#').Append(' ');
|
||||
}
|
||||
else if (anyDisabled)
|
||||
{
|
||||
lineBuilder.Append(' ').Append(' ');
|
||||
}
|
||||
|
||||
lineBuilder.Append(e.Address.PadRight(addressPadding));
|
||||
lineBuilder.Append(string.Join(' ', e.Hosts).PadRight(hostsPadding));
|
||||
|
||||
if (e.Comment != string.Empty)
|
||||
{
|
||||
lineBuilder.Append('#').Append(' ');
|
||||
lineBuilder.Append(e.Comment);
|
||||
}
|
||||
|
||||
lines.Add(lineBuilder.ToString().TrimEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(additionalLines))
|
||||
{
|
||||
if (_userSettings.AdditionalLinesPosition == HostsAdditionalLinesPosition.Top)
|
||||
{
|
||||
lines.Insert(0, additionalLines);
|
||||
}
|
||||
else if (_userSettings.AdditionalLinesPosition == HostsAdditionalLinesPosition.Bottom)
|
||||
{
|
||||
lines.Add(additionalLines);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _asyncLock.WaitAsync();
|
||||
_fileSystemWatcher.EnableRaisingEvents = false;
|
||||
|
||||
if (!_backupDone && Exists())
|
||||
{
|
||||
_fileSystem.File.Copy(HostsFilePath, HostsFilePath + _backupSuffix + DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture));
|
||||
_backupDone = true;
|
||||
}
|
||||
|
||||
await _fileSystem.File.WriteAllLinesAsync(HostsFilePath, lines, Encoding);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileSystemWatcher.EnableRaisingEvents = true;
|
||||
_asyncLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PingAsync(string address)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var ping = new Ping();
|
||||
var reply = await ping.SendPingAsync(address, 4000); // 4000 is the default ping timeout for ping.exe
|
||||
return reply.Status == IPStatus.Success;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void CleanupBackup()
|
||||
{
|
||||
Directory.GetFiles(Path.GetDirectoryName(HostsFilePath), $"*{_backupSuffix}*")
|
||||
.Select(f => new FileInfo(f))
|
||||
.Where(f => f.CreationTime < DateTime.Now.AddDays(-15))
|
||||
.ToList()
|
||||
.ForEach(f => f.Delete());
|
||||
}
|
||||
|
||||
public void OpenHostsFile()
|
||||
{
|
||||
var notepadFallback = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Try to open in default editor
|
||||
var key = Registry.ClassesRoot.OpenSubKey("SystemFileAssociations\\text\\shell\\edit\\command");
|
||||
if (key != null)
|
||||
{
|
||||
var commandPattern = key.GetValue(string.Empty).ToString(); // Default value
|
||||
var file = null as string;
|
||||
var args = null as string;
|
||||
|
||||
if (commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
|
||||
if (endQuoteIndex != -1)
|
||||
{
|
||||
file = commandPattern[1..endQuoteIndex];
|
||||
args = commandPattern[(endQuoteIndex + 1)..].Trim();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spaceIndex = commandPattern.IndexOf(' ');
|
||||
if (spaceIndex != -1)
|
||||
{
|
||||
file = commandPattern[..spaceIndex];
|
||||
args = commandPattern[(spaceIndex + 1)..].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (file != null && args != null)
|
||||
{
|
||||
args = args.Replace("%1", HostsFilePath);
|
||||
Process.Start(new ProcessStartInfo(file, args));
|
||||
}
|
||||
else
|
||||
{
|
||||
notepadFallback = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to open default editor", ex);
|
||||
notepadFallback = true;
|
||||
}
|
||||
|
||||
if (notepadFallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("notepad.exe", HostsFilePath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveReadOnly()
|
||||
{
|
||||
var fileInfo = _fileSystem.FileInfo.FromFileName(HostsFilePath);
|
||||
if (fileInfo.IsReadOnly)
|
||||
{
|
||||
fileInfo.IsReadOnly = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
_fileSystemWatcher.EnableRaisingEvents = false;
|
||||
FileChanged?.Invoke(this, EventArgs.Empty);
|
||||
_fileSystemWatcher.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_asyncLock.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/modules/Hosts/HostsUILib/Helpers/IElevationHelper.cs
Normal file
11
src/modules/Hosts/HostsUILib/Helpers/IElevationHelper.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
// 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 HostsUILib.Helpers
|
||||
{
|
||||
public interface IElevationHelper
|
||||
{
|
||||
bool IsElevated { get; }
|
||||
}
|
||||
}
|
||||
30
src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs
Normal file
30
src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using HostsUILib.Models;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
public interface IHostsService : IDisposable
|
||||
{
|
||||
string HostsFilePath { get; }
|
||||
|
||||
event EventHandler FileChanged;
|
||||
|
||||
Task<HostsData> ReadAsync();
|
||||
|
||||
Task WriteAsync(string additionalLines, IEnumerable<Entry> entries);
|
||||
|
||||
Task<bool> PingAsync(string address);
|
||||
|
||||
void CleanupBackup();
|
||||
|
||||
void OpenHostsFile();
|
||||
|
||||
void RemoveReadOnly();
|
||||
}
|
||||
}
|
||||
23
src/modules/Hosts/HostsUILib/Helpers/ILogger.cs
Normal file
23
src/modules/Hosts/HostsUILib/Helpers/ILogger.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
public void LogError(string message);
|
||||
|
||||
public void LogError(string message, Exception ex);
|
||||
|
||||
public void LogWarning(string message);
|
||||
|
||||
public void LogInfo(string message);
|
||||
|
||||
public void LogDebug(string message);
|
||||
|
||||
public void LogTrace();
|
||||
}
|
||||
}
|
||||
10
src/modules/Hosts/HostsUILib/Helpers/LoggerInstance.cs
Normal file
10
src/modules/Hosts/HostsUILib/Helpers/LoggerInstance.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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 HostsUILib.Helpers
|
||||
{
|
||||
public static class LoggerInstance
|
||||
{
|
||||
public static ILogger Logger { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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 Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
internal static class ResourceLoaderInstance
|
||||
{
|
||||
internal static ResourceLoader ResourceLoader { get; private set; }
|
||||
|
||||
static ResourceLoaderInstance()
|
||||
{
|
||||
ResourceLoader = new Microsoft.Windows.ApplicationModel.Resources.ResourceLoader("PowerToys.HostsUILib.pri", "PowerToys.HostsUILib/Resources");
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/modules/Hosts/HostsUILib/Helpers/StringHelper.cs
Normal file
14
src/modules/Hosts/HostsUILib/Helpers/StringHelper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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 HostsUILib.Helpers
|
||||
{
|
||||
public static class StringHelper
|
||||
{
|
||||
public static string GetHostsWithComment(string hosts, string comment)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(comment) ? hosts : string.Concat(hosts, " - ", comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/modules/Hosts/HostsUILib/Helpers/ValidationHelper.cs
Normal file
68
src/modules/Hosts/HostsUILib/Helpers/ValidationHelper.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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.Text.RegularExpressions;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
public static class ValidationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the address is a valid IPv4
|
||||
/// </summary>
|
||||
public static bool ValidIPv4(string address)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var regex = new Regex("^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
|
||||
return regex.IsMatch(address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the address is a valid IPv6
|
||||
/// </summary>
|
||||
public static bool ValidIPv6(string address)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var regex = new Regex("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$");
|
||||
return regex.IsMatch(address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the hosts are valid
|
||||
/// </summary>
|
||||
public static bool ValidHosts(string hosts, bool validateHostsLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hosts))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var splittedHosts = hosts.Split(' ');
|
||||
|
||||
if (validateHostsLength && splittedHosts.Length > Consts.MaxHostsCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var host in splittedHosts)
|
||||
{
|
||||
if (Uri.CheckHostName(host) == UriHostNameType.Unknown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/modules/Hosts/HostsUILib/Helpers/VisualTreeUtils.cs
Normal file
67
src/modules/Hosts/HostsUILib/Helpers/VisualTreeUtils.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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 Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
// Taken from https://github.com/microsoft/microsoft-ui-xaml/blob/main/test/MUXControlsTestApp/Utilities/VisualTreeUtils.cs
|
||||
// Original copyright header:
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See LICENSE in the project root for license information.
|
||||
public static class VisualTreeUtils
|
||||
{
|
||||
public static T FindVisualChildByType<T>(this DependencyObject element)
|
||||
where T : DependencyObject
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (element is T elementAsT)
|
||||
{
|
||||
return elementAsT;
|
||||
}
|
||||
|
||||
int childrenCount = VisualTreeHelper.GetChildrenCount(element);
|
||||
for (int i = 0; i < childrenCount; i++)
|
||||
{
|
||||
var result = VisualTreeHelper.GetChild(element, i).FindVisualChildByType<T>();
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static FrameworkElement FindVisualChildByName(this DependencyObject element, string name)
|
||||
{
|
||||
if (element == null || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (element is FrameworkElement elementAsFE && elementAsFE.Name == name)
|
||||
{
|
||||
return elementAsFE;
|
||||
}
|
||||
|
||||
int childrenCount = VisualTreeHelper.GetChildrenCount(element);
|
||||
for (int i = 0; i < childrenCount; i++)
|
||||
{
|
||||
var result = VisualTreeHelper.GetChild(element, i).FindVisualChildByName(name);
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
725
src/modules/Hosts/HostsUILib/HostsMainPage.xaml
Normal file
725
src/modules/Hosts/HostsUILib/HostsMainPage.xaml
Normal file
@@ -0,0 +1,725 @@
|
||||
<Page
|
||||
x:Class="HostsUILib.Views.HostsMainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:HostsUILib.Helpers"
|
||||
xmlns:i="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:local="using:Hosts.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:HostsUILib.Models"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
x:Name="Page"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="Loaded">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ReadHostsCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
|
||||
<Page.Resources>
|
||||
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<StaticResource x:Key="SubtleButtonBackground" ResourceKey="SubtleFillColorTransparent" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundPointerOver" ResourceKey="SubtleFillColorSecondary" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundPressed" ResourceKey="SubtleFillColorTertiary" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundDisabled" ResourceKey="SubtleFillColorTransparent" />
|
||||
|
||||
<StaticResource x:Key="SubtleButtonBorderBrush" ResourceKey="SubtleFillColorTransparent" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushPointerOver" ResourceKey="SubtleFillColorSecondary" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushPressed" ResourceKey="SubtleFillColorTertiary" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushDisabled" ResourceKey="SubtleFillColorTransparent" />
|
||||
|
||||
<StaticResource x:Key="SubtleButtonForeground" ResourceKey="TextFillColorPrimary" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundPointerOver" ResourceKey="TextFillColorPrimary" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundPressed" ResourceKey="TextFillColorSecondary" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundDisabled" ResourceKey="TextFillColorDisabled" />
|
||||
</ResourceDictionary>
|
||||
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<StaticResource x:Key="SubtleButtonBackground" ResourceKey="SubtleFillColorTransparent" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundPointerOver" ResourceKey="SubtleFillColorSecondary" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundPressed" ResourceKey="SubtleFillColorTertiary" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundDisabled" ResourceKey="SubtleFillColorTransparent" />
|
||||
|
||||
<StaticResource x:Key="SubtleButtonBorderBrush" ResourceKey="SubtleFillColorTransparent" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushPointerOver" ResourceKey="SubtleFillColorSecondary" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushPressed" ResourceKey="SubtleFillColorTertiary" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushDisabled" ResourceKey="SubtleFillColorTransparent" />
|
||||
|
||||
<StaticResource x:Key="SubtleButtonForeground" ResourceKey="TextFillColorPrimary" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundPointerOver" ResourceKey="TextFillColorPrimary" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundPressed" ResourceKey="TextFillColorSecondary" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundDisabled" ResourceKey="TextFillColorDisabled" />
|
||||
</ResourceDictionary>
|
||||
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<StaticResource x:Key="SubtleButtonBackground" ResourceKey="SystemColorWindowColorBrush" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundPointerOver" ResourceKey="SystemColorHighlightTextColorBrush" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundPressed" ResourceKey="SystemColorWindowColorBrush" />
|
||||
<StaticResource x:Key="SubtleButtonBackgroundDisabled" ResourceKey="SystemControlBackgroundBaseLowBrush" />
|
||||
|
||||
<StaticResource x:Key="SubtleButtonBorderBrush" ResourceKey="SystemColorWindowColorBrush" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushPointerOver" ResourceKey="SystemColorHighlightColorBrush" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushPressed" ResourceKey="SystemColorHighlightColorBrush" />
|
||||
<StaticResource x:Key="SubtleButtonBorderBrushDisabled" ResourceKey="SystemColorGrayTextColor" />
|
||||
|
||||
<StaticResource x:Key="SubtleButtonForeground" ResourceKey="SystemColorButtonTextColorBrush" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundPointerOver" ResourceKey="SystemControlHighlightBaseHighBrush" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundPressed" ResourceKey="SystemControlHighlightBaseHighBrush" />
|
||||
<StaticResource x:Key="SubtleButtonForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
|
||||
</ResourceDictionary>
|
||||
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<Style x:Key="SubtleButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{ThemeResource SubtleButtonBackground}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource SubtleButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource SubtleButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Foreground="{TemplateBinding Foreground}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
|
||||
<tkconverters:StringVisibilityConverter
|
||||
x:Key="StringVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
<tkconverters:BoolNegationConverter x:Key="BoolNegationConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter
|
||||
x:Key="BoolToInvertedVisibilityConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
<tkconverters:DoubleToVisibilityConverter
|
||||
x:Key="DoubleToVisibilityConverter"
|
||||
FalseValue="Visible"
|
||||
GreaterThan="0"
|
||||
TrueValue="Collapsed" />
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid Margin="16" RowSpacing="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Buttons -->
|
||||
<RowDefinition Height="*" />
|
||||
<!-- Content -->
|
||||
</Grid.RowDefinitions>
|
||||
<Grid>
|
||||
<!-- Buttons -->
|
||||
<Button x:Uid="AddEntryBtn" Command="{x:Bind NewDialogCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon
|
||||
x:Name="Icon"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock x:Uid="AddEntry" />
|
||||
</StackPanel>
|
||||
<Button.KeyboardAccelerators>
|
||||
<KeyboardAccelerator Key="N" Modifiers="Control" />
|
||||
</Button.KeyboardAccelerators>
|
||||
</Button>
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="4">
|
||||
<Button
|
||||
x:Uid="AdditionalLinesBtn"
|
||||
Height="32"
|
||||
Command="{x:Bind AdditionalLinesDialogCommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
|
||||
<Button
|
||||
x:Uid="FilterBtn"
|
||||
Height="32"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Content>
|
||||
<Grid>
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
<InfoBadge
|
||||
Width="10"
|
||||
Height="10"
|
||||
Margin="0,-4,-4,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Visibility="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<Flyout ShouldConstrainToRootBounds="False">
|
||||
<StackPanel
|
||||
Width="320"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="12">
|
||||
<AutoSuggestBox
|
||||
x:Uid="AddressFilter"
|
||||
PlaceholderText=""
|
||||
QueryIcon="Find"
|
||||
Text="{x:Bind ViewModel.AddressFilter, Mode=TwoWay}">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="TextChanged">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<AutoSuggestBox
|
||||
x:Uid="HostsFilter"
|
||||
QueryIcon="Find"
|
||||
Text="{x:Bind ViewModel.HostsFilter, Mode=TwoWay}">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="TextChanged">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<AutoSuggestBox
|
||||
x:Uid="CommentFilter"
|
||||
QueryIcon="Find"
|
||||
Text="{x:Bind ViewModel.CommentFilter, Mode=TwoWay}">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="TextChanged">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<ToggleSwitch x:Uid="ShowOnlyDuplicates" IsOn="{x:Bind ViewModel.ShowOnlyDuplicates, Mode=TwoWay}" />
|
||||
<Button
|
||||
x:Uid="ClearFiltersBtn"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.ClearFiltersCommand}"
|
||||
IsEnabled="{x:Bind ViewModel.Filtered, Mode=OneWay}"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
x:Uid="OpenHostsFileBtn"
|
||||
Height="32"
|
||||
Command="{x:Bind ViewModel.OpenHostsFileCommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
|
||||
|
||||
<Button
|
||||
x:Uid="SettingsBtn"
|
||||
Height="32"
|
||||
Command="{x:Bind ViewModel.OpenSettingsCommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
https://github.com/microsoft/microsoft-ui-xaml/issues/7690
|
||||
AllowDrop="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
|
||||
CanDragItems="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
|
||||
CanReorderItems="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
|
||||
-->
|
||||
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsLoading, Converter={StaticResource BoolToInvertedVisibilityConverter}, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ListView
|
||||
x:Name="Entries"
|
||||
x:Uid="Entries"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
GotFocus="Entries_GotFocus"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Entries_ItemClick"
|
||||
ItemsSource="{x:Bind ViewModel.Entries, Mode=TwoWay}"
|
||||
RightTapped="Entries_RightTapped"
|
||||
SelectedItem="{x:Bind ViewModel.Selected, Mode=TwoWay}">
|
||||
<ListView.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="Edit"
|
||||
Click="Edit_Click"
|
||||
Icon="Edit">
|
||||
<MenuFlyoutItem.KeyboardAccelerators>
|
||||
<KeyboardAccelerator
|
||||
Key="E"
|
||||
Modifiers="Control"
|
||||
ScopeOwner="{x:Bind Entries}" />
|
||||
</MenuFlyoutItem.KeyboardAccelerators>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem x:Uid="Duplicate" Click="Duplicate_Click">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
<MenuFlyoutItem.KeyboardAccelerators>
|
||||
<KeyboardAccelerator
|
||||
Key="D"
|
||||
Modifiers="Control"
|
||||
ScopeOwner="{x:Bind Entries}" />
|
||||
</MenuFlyoutItem.KeyboardAccelerators>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="Ping"
|
||||
Click="Ping_Click"
|
||||
Icon="TwoBars">
|
||||
<MenuFlyoutItem.KeyboardAccelerators>
|
||||
<KeyboardAccelerator
|
||||
Key="P"
|
||||
Modifiers="Control"
|
||||
ScopeOwner="{x:Bind Entries}" />
|
||||
</MenuFlyoutItem.KeyboardAccelerators>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveUp"
|
||||
Click="ReorderButtonUp_Click"
|
||||
IsEnabled="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveDown"
|
||||
Click="ReorderButtonDown_Click"
|
||||
IsEnabled="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="Delete"
|
||||
Click="Delete_Click"
|
||||
Icon="Delete">
|
||||
<MenuFlyoutItem.KeyboardAccelerators>
|
||||
<KeyboardAccelerator Key="Delete" ScopeOwner="{x:Bind Entries}" />
|
||||
</MenuFlyoutItem.KeyboardAccelerators>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
</ListView.ContextFlyout>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Entry">
|
||||
<Grid
|
||||
AutomationProperties.Name="{x:Bind Address, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="256" />
|
||||
<!-- Address -->
|
||||
<ColumnDefinition Width="*" />
|
||||
<!-- Comment -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- Status -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- Duplicate -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- ToggleSwitch -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- DeleteEntry -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Address, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind helpers:StringHelper.GetHostsWithComment(Hosts, Comment), Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<ProgressRing
|
||||
Grid.Column="2"
|
||||
Width="20"
|
||||
Height="20"
|
||||
IsActive="{x:Bind Pinging, Mode=OneWay}" />
|
||||
<FontIcon
|
||||
x:Name="PingIcon"
|
||||
x:Uid="PingIcon"
|
||||
Grid.Column="2"
|
||||
FontSize="16"
|
||||
Visibility="Collapsed">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:DataTriggerBehavior
|
||||
Binding="{x:Bind Ping, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Glyph"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Visibility"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="Visible" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Foreground"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="{StaticResource SystemFillColorSuccessBrush}" />
|
||||
</ic:DataTriggerBehavior>
|
||||
<ic:DataTriggerBehavior
|
||||
Binding="{x:Bind Ping, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Glyph"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Visibility"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="Visible" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Foreground"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="{StaticResource SystemFillColorCriticalBrush}" />
|
||||
</ic:DataTriggerBehavior>
|
||||
<ic:DataTriggerBehavior
|
||||
Binding="{x:Bind Ping, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="{x:Null}">
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Visibility"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="Collapsed" />
|
||||
</ic:DataTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</FontIcon>
|
||||
<FontIcon
|
||||
x:Uid="DuplicateEntryIcon"
|
||||
Grid.Column="3"
|
||||
FontSize="16"
|
||||
Foreground="{StaticResource SystemControlErrorTextForegroundBrush}"
|
||||
Glyph=""
|
||||
Visibility="{x:Bind Duplicate, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<ToggleSwitch
|
||||
x:Uid="ActiveToggle"
|
||||
Grid.Column="4"
|
||||
Width="40"
|
||||
MinWidth="0"
|
||||
HorizontalAlignment="Right"
|
||||
GotFocus="Entries_GotFocus"
|
||||
IsOn="{x:Bind Active, Mode=TwoWay}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
<Button
|
||||
x:Uid="DeleteEntryBtn"
|
||||
Grid.Column="5"
|
||||
Height="32"
|
||||
Click="Delete_Click"
|
||||
CommandParameter="{x:Bind (models:Entry)}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
GotFocus="Entries_GotFocus"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{x:Bind ViewModel.Entries.Count, Mode=OneWay, Converter={StaticResource DoubleToVisibilityConverter}}">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8"
|
||||
Visibility="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<FontIcon FontSize="32" Glyph="" />
|
||||
<TextBlock
|
||||
x:Uid="EmptyHosts"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
<HyperlinkButton
|
||||
x:Uid="AddEntryLink"
|
||||
HorizontalAlignment="Center"
|
||||
Command="{x:Bind NewDialogCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8"
|
||||
Visibility="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<FontIcon FontSize="32" Glyph="" />
|
||||
<TextBlock
|
||||
x:Uid="EmptyFilterResults"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
<HyperlinkButton
|
||||
x:Uid="ClearFiltersLink"
|
||||
HorizontalAlignment="Center"
|
||||
Command="{x:Bind ViewModel.ClearFiltersCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1">
|
||||
<InfoBar
|
||||
x:Uid="FileSaveError"
|
||||
Margin="0,8,0,0"
|
||||
IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}"
|
||||
Message="{x:Bind ViewModel.ErrorMessage, Mode=TwoWay}"
|
||||
Severity="Error"
|
||||
Visibility="{x:Bind ViewModel.Error, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="MakeWritable"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.OverwriteHostsCommand}"
|
||||
Visibility="{x:Bind ViewModel.IsReadOnly, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
<InfoBar
|
||||
x:Uid="FileChanged"
|
||||
Margin="0,8,0,0"
|
||||
IsOpen="{x:Bind ViewModel.FileChanged, Mode=TwoWay}"
|
||||
Severity="Informational"
|
||||
Visibility="{x:Bind ViewModel.FileChanged, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="Reload"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.ReadHostsCommand}" />
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ProgressRing
|
||||
Grid.Row="1"
|
||||
Width="48"
|
||||
Height="48"
|
||||
IsActive="{x:Bind ViewModel.IsLoading, Mode=OneWay}" />
|
||||
|
||||
<ContentDialog
|
||||
x:Name="EntryDialog"
|
||||
x:Uid="EntryDialog"
|
||||
IsPrimaryButtonEnabled="{Binding Valid, Mode=OneWay}"
|
||||
Loaded="ContentDialog_Loaded_ApplyMargin"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<ContentDialog.DataContext>
|
||||
<models:Entry />
|
||||
</ContentDialog.DataContext>
|
||||
<ScrollViewer>
|
||||
<StackPanel
|
||||
MinWidth="320"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="12">
|
||||
<TextBox
|
||||
x:Uid="Address"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{Binding Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<TextBox.Description>
|
||||
<StackPanel
|
||||
Margin="0,4"
|
||||
Orientation="Horizontal"
|
||||
Spacing="4"
|
||||
Visibility="{Binding IsAddressValid, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<FontIcon
|
||||
Margin="0,0,0,0"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock x:Uid="EntryAddressIsInvalidWarning" />
|
||||
</StackPanel>
|
||||
</TextBox.Description>
|
||||
</TextBox>
|
||||
<TextBox
|
||||
x:Uid="Hosts"
|
||||
AcceptsReturn="False"
|
||||
IsSpellCheckEnabled="False"
|
||||
ScrollViewer.IsVerticalRailEnabled="True"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Enabled"
|
||||
Text="{Binding Hosts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextWrapping="Wrap">
|
||||
<TextBox.Description>
|
||||
<StackPanel
|
||||
Margin="0,4"
|
||||
Orientation="Horizontal"
|
||||
Spacing="4"
|
||||
Visibility="{Binding IsHostsValid, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<FontIcon
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock x:Uid="EntryHostsIsInvalidWarning" />
|
||||
</StackPanel>
|
||||
</TextBox.Description>
|
||||
</TextBox>
|
||||
<TextBox
|
||||
x:Uid="Comment"
|
||||
AcceptsReturn="False"
|
||||
IsSpellCheckEnabled="False"
|
||||
ScrollViewer.IsVerticalRailEnabled="True"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Enabled"
|
||||
Text="{Binding Comment, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextWrapping="Wrap" />
|
||||
<ToggleSwitch
|
||||
x:Uid="Active"
|
||||
IsOn="{Binding Active, Mode=TwoWay}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
|
||||
<ContentDialog
|
||||
x:Name="DeleteDialog"
|
||||
x:Uid="DeleteDialog"
|
||||
PrimaryButtonCommand="{x:Bind DeleteCommand}"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Uid="DeleteDialogAreYouSure" />
|
||||
</ContentDialog>
|
||||
|
||||
<ContentDialog
|
||||
x:Name="AdditionalLinesDialog"
|
||||
x:Uid="AdditionalLinesDialog"
|
||||
Loaded="ContentDialog_Loaded_ApplyMargin"
|
||||
PrimaryButtonCommand="{x:Bind UpdateAdditionalLinesCommand}"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
|
||||
<TextBox
|
||||
x:Name="AdditionalLines"
|
||||
MinHeight="40"
|
||||
Padding="16,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
AcceptsReturn="True"
|
||||
ScrollViewer.IsVerticalRailEnabled="True"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Enabled"
|
||||
TextWrapping="Wrap" />
|
||||
</ContentDialog>
|
||||
|
||||
<TeachingTip
|
||||
x:Uid="TooManyHostsTeachingTip"
|
||||
IconSource="{ui:FontIconSource Glyph=}"
|
||||
IsOpen="{x:Bind ViewModel.ShowSplittedEntriesTooltip, Mode=OneWay}"
|
||||
PlacementMargin="20"
|
||||
PreferredPlacement="Top">
|
||||
<TeachingTip.Content>
|
||||
<TextBlock x:Uid="TooManyHostsTeachingTipContent" TextWrapping="Wrap" />
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
</Grid>
|
||||
</Page>
|
||||
234
src/modules/Hosts/HostsUILib/HostsMainPage.xaml.cs
Normal file
234
src/modules/Hosts/HostsUILib/HostsMainPage.xaml.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
// 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.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HostsUILib.Helpers;
|
||||
using HostsUILib.Models;
|
||||
using HostsUILib.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace HostsUILib.Views
|
||||
{
|
||||
public partial class HostsMainPage : Page
|
||||
{
|
||||
public MainViewModel ViewModel { get; private set; }
|
||||
|
||||
public ICommand NewDialogCommand => new AsyncRelayCommand(OpenNewDialogAsync);
|
||||
|
||||
public ICommand AdditionalLinesDialogCommand => new AsyncRelayCommand(OpenAdditionalLinesDialogAsync);
|
||||
|
||||
public ICommand AddCommand => new RelayCommand(Add);
|
||||
|
||||
public ICommand UpdateCommand => new RelayCommand(Update);
|
||||
|
||||
public ICommand DeleteCommand => new RelayCommand(Delete);
|
||||
|
||||
public ICommand UpdateAdditionalLinesCommand => new RelayCommand(UpdateAdditionalLines);
|
||||
|
||||
public ICommand ExitCommand => new RelayCommand(() => { Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread().TryEnqueue(Application.Current.Exit); });
|
||||
|
||||
public HostsMainPage(MainViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
ViewModel = viewModel;
|
||||
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
|
||||
private async Task OpenNewDialogAsync()
|
||||
{
|
||||
await ShowAddDialogAsync();
|
||||
}
|
||||
|
||||
private async Task OpenAdditionalLinesDialogAsync()
|
||||
{
|
||||
AdditionalLines.Text = ViewModel.AdditionalLines;
|
||||
await AdditionalLinesDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private async void Entries_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
Entry entry = e.ClickedItem as Entry;
|
||||
ViewModel.Selected = entry;
|
||||
await ShowEditDialogAsync(entry);
|
||||
}
|
||||
|
||||
public async Task ShowEditDialogAsync(Entry entry)
|
||||
{
|
||||
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
EntryDialog.Title = resourceLoader.GetString("UpdateEntry_Title");
|
||||
EntryDialog.PrimaryButtonText = resourceLoader.GetString("UpdateBtn");
|
||||
EntryDialog.PrimaryButtonCommand = UpdateCommand;
|
||||
var clone = ViewModel.Selected.Clone();
|
||||
EntryDialog.DataContext = clone;
|
||||
await EntryDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void Add()
|
||||
{
|
||||
ViewModel.Add(EntryDialog.DataContext as Entry);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
ViewModel.Update(Entries.SelectedIndex, EntryDialog.DataContext as Entry);
|
||||
}
|
||||
|
||||
private void Delete()
|
||||
{
|
||||
ViewModel.DeleteSelected();
|
||||
}
|
||||
|
||||
private void UpdateAdditionalLines()
|
||||
{
|
||||
ViewModel.UpdateAdditionalLines(AdditionalLines.Text);
|
||||
}
|
||||
|
||||
private async void Delete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Entries.SelectedItem is Entry entry)
|
||||
{
|
||||
ViewModel.Selected = entry;
|
||||
DeleteDialog.Title = entry.Address;
|
||||
await DeleteDialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Edit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Entries.SelectedItem is Entry entry)
|
||||
{
|
||||
await ShowEditDialogAsync(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Duplicate_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Entries.SelectedItem is Entry entry)
|
||||
{
|
||||
await ShowAddDialogAsync(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Ping_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Entries.SelectedItem is Entry entry)
|
||||
{
|
||||
ViewModel.Selected = entry;
|
||||
await ViewModel.PingSelectedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.ReadHosts();
|
||||
|
||||
var userSettings = ViewModel.UserSettings;
|
||||
if (userSettings.ShowStartupWarning)
|
||||
{
|
||||
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
var dialog = new ContentDialog();
|
||||
|
||||
dialog.XamlRoot = XamlRoot;
|
||||
dialog.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style;
|
||||
dialog.Title = resourceLoader.GetString("WarningDialog_Title");
|
||||
dialog.Content = new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("WarningDialog_Text"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
dialog.PrimaryButtonText = resourceLoader.GetString("WarningDialog_AcceptBtn");
|
||||
dialog.PrimaryButtonStyle = Application.Current.Resources["AccentButtonStyle"] as Style;
|
||||
dialog.CloseButtonText = resourceLoader.GetString("WarningDialog_QuitBtn");
|
||||
dialog.CloseButtonCommand = ExitCommand;
|
||||
|
||||
await dialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReorderButtonUp_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Entries.SelectedItem is Entry entry)
|
||||
{
|
||||
var index = ViewModel.Entries.IndexOf(entry);
|
||||
if (index > 0)
|
||||
{
|
||||
ViewModel.Move(index, index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReorderButtonDown_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Entries.SelectedItem is Entry entry)
|
||||
{
|
||||
var index = ViewModel.Entries.IndexOf(entry);
|
||||
if (index < ViewModel.Entries.Count - 1)
|
||||
{
|
||||
ViewModel.Move(index, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Focus the first item when the list view gets the focus with keyboard
|
||||
/// </summary>
|
||||
private void Entries_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var element = sender as FrameworkElement;
|
||||
var entry = element.DataContext as Entry;
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
ViewModel.Selected = entry;
|
||||
}
|
||||
else if (Entries.SelectedItem == null && Entries.Items.Count > 0)
|
||||
{
|
||||
Entries.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void Entries_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||
{
|
||||
var entry = (e.OriginalSource as FrameworkElement).DataContext as Entry;
|
||||
ViewModel.Selected = entry;
|
||||
}
|
||||
|
||||
private async Task ShowAddDialogAsync(Entry template = null)
|
||||
{
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
EntryDialog.Title = resourceLoader.GetString("AddNewEntryDialog_Title");
|
||||
EntryDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
|
||||
EntryDialog.PrimaryButtonCommand = AddCommand;
|
||||
|
||||
EntryDialog.DataContext = template == null
|
||||
? new Entry(ViewModel.NextId, string.Empty, string.Empty, string.Empty, true)
|
||||
: new Entry(ViewModel.NextId, template.Address, template.Hosts, template.Comment, template.Active);
|
||||
|
||||
await EntryDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void ContentDialog_Loaded_ApplyMargin(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Based on the template from dev/CommonStyles/ContentDialog_themeresources.xaml in https://github.com/microsoft/microsoft-ui-xaml
|
||||
var border = VisualTreeUtils.FindVisualChildByName(sender as ContentDialog, "BackgroundElement") as Border;
|
||||
if (border is not null)
|
||||
{
|
||||
border.Margin = new Thickness(0, 32, 0, 0); // Should be the size reserved for the title bar as in MainWindow.
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Couldn't set the margin for a content dialog. It will appear on top of the title bar.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/modules/Hosts/HostsUILib/HostsUILib.csproj
Normal file
69
src/modules/Hosts/HostsUILib/HostsUILib.csproj
Normal file
@@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Condition="exists('..\..\..\Version.props')" Project="..\..\..\Version.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.20348</TargetFramework>
|
||||
<RootNamespace>HostsUILib</RootNamespace>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<AssemblyName>PowerToys.HostsUILib</AssemblyName>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>PowerToys.HostsUILib.pri</ProjectPriFileName>
|
||||
|
||||
<GenerateLibraryLayout>true</GenerateLibraryLayout>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(OutDir)\PowerToys.HostsUILib.pri" Pack="True" PackageCopyToOutput="true" PackagePath="lib/$(TargetFramework)" />
|
||||
<None Include="$(OutDir)\PowerToys.HostsUILib.pri" Pack="True" PackageCopyToOutput="True" PackagePath="contentFiles\any\$(TargetFramework)" />
|
||||
<XBFFile Include="$(OutDir)**\*.xbf" />
|
||||
<None Include="@(XBFFile)" Pack="True" PackageCopyToOutput="True" PackagePath="contentFiles\any\$(TargetFramework)" />
|
||||
<None Include="$(OutDir)\PowerToys.HostsUILib.pdb" Pack="True" PackageCopyToOutput="true" PackagePath="lib/$(TargetFramework)" />
|
||||
|
||||
<None Include="Assets\**\*.png" Pack="true" PackageCopyToOutput="true" PackagePath="contentFiles\any\$(TargetFramework)\Assets" />
|
||||
<None Include="Assets\**\*.ico" Pack="true" PackageCopyToOutput="true" PackagePath="contentFiles\any\$(TargetFramework)\Assets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="Assets\**\*.png" Pack="false" />
|
||||
<Content Remove="Assets\**\*.ico" Pack="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Optimize>false</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Collections" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
13
src/modules/Hosts/HostsUILib/Models/AddressType.cs
Normal file
13
src/modules/Hosts/HostsUILib/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 HostsUILib.Models
|
||||
{
|
||||
public enum AddressType
|
||||
{
|
||||
Invalid = 0,
|
||||
IPv4 = 1,
|
||||
IPv6 = 2,
|
||||
}
|
||||
}
|
||||
188
src/modules/Hosts/HostsUILib/Models/Entry.cs
Normal file
188
src/modules/Hosts/HostsUILib/Models/Entry.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
// 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.Net;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using HostsUILib.Helpers;
|
||||
|
||||
namespace HostsUILib.Models
|
||||
{
|
||||
public partial class Entry : ObservableObject
|
||||
{
|
||||
private static readonly char[] _spaceCharacters = new char[] { ' ', '\t' };
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Valid))]
|
||||
[NotifyPropertyChangedFor(nameof(IsAddressValid))]
|
||||
private string _address;
|
||||
|
||||
partial void OnAddressChanged(string value)
|
||||
{
|
||||
if (ValidationHelper.ValidIPv4(value))
|
||||
{
|
||||
Type = AddressType.IPv4;
|
||||
}
|
||||
else if (ValidationHelper.ValidIPv6(value))
|
||||
{
|
||||
Type = AddressType.IPv6;
|
||||
}
|
||||
else
|
||||
{
|
||||
Type = AddressType.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Valid))]
|
||||
[NotifyPropertyChangedFor(nameof(IsHostsValid))]
|
||||
private string _hosts;
|
||||
|
||||
partial void OnHostsChanged(string value)
|
||||
{
|
||||
SplittedHosts = value.Split(' ');
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Valid))]
|
||||
private string _comment;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _active;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool? _ping;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _pinging;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _duplicate;
|
||||
|
||||
public bool Valid => Validate(true);
|
||||
|
||||
public bool IsAddressValid => ValidateAddressField();
|
||||
|
||||
public bool IsHostsValid => ValidateHostsField(true);
|
||||
|
||||
public string Line { get; private set; }
|
||||
|
||||
public AddressType Type { get; private set; }
|
||||
|
||||
public string[] SplittedHosts { get; private set; }
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
public Entry()
|
||||
{
|
||||
}
|
||||
|
||||
public Entry(int id, string line)
|
||||
{
|
||||
Id = id;
|
||||
Line = line.Trim();
|
||||
Parse();
|
||||
}
|
||||
|
||||
public Entry(int id, string address, string hosts, string comment, bool active)
|
||||
{
|
||||
Id = id;
|
||||
Address = address.Trim();
|
||||
Hosts = hosts.Trim();
|
||||
Comment = comment.Trim();
|
||||
Active = active;
|
||||
}
|
||||
|
||||
public void Parse()
|
||||
{
|
||||
Active = !Line.StartsWith("#", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
var lineSplit = Line.TrimStart(' ', '#').Split('#');
|
||||
|
||||
if (lineSplit.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var addressHost = lineSplit[0];
|
||||
|
||||
var addressHostSplit = addressHost.Split(_spaceCharacters, StringSplitOptions.RemoveEmptyEntries);
|
||||
var hostsBuilder = new StringBuilder();
|
||||
var commentBuilder = new StringBuilder();
|
||||
|
||||
for (var i = 0; i < addressHostSplit.Length; i++)
|
||||
{
|
||||
var element = addressHostSplit[i].Trim();
|
||||
|
||||
if (i == 0 && IPAddress.TryParse(element, out var _) && (element.Contains(':') || element.Contains('.')))
|
||||
{
|
||||
Address = element;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hostsBuilder.Length > 0)
|
||||
{
|
||||
hostsBuilder.Append(' ');
|
||||
}
|
||||
|
||||
hostsBuilder.Append(element);
|
||||
}
|
||||
}
|
||||
|
||||
Hosts = hostsBuilder.ToString();
|
||||
|
||||
for (var i = 1; i < lineSplit.Length; i++)
|
||||
{
|
||||
if (commentBuilder.Length > 0)
|
||||
{
|
||||
commentBuilder.Append('#');
|
||||
}
|
||||
|
||||
commentBuilder.Append(lineSplit[i]);
|
||||
}
|
||||
|
||||
Comment = commentBuilder.ToString().Trim();
|
||||
}
|
||||
|
||||
public Entry Clone()
|
||||
{
|
||||
return new Entry
|
||||
{
|
||||
Line = Line,
|
||||
Address = Address,
|
||||
Hosts = Hosts,
|
||||
Comment = Comment,
|
||||
Active = Active,
|
||||
};
|
||||
}
|
||||
|
||||
private bool ValidateAddressField()
|
||||
{
|
||||
return Type != AddressType.Invalid;
|
||||
}
|
||||
|
||||
private bool ValidateHostsField(bool validateHostsLength)
|
||||
{
|
||||
return ValidationHelper.ValidHosts(Hosts, validateHostsLength);
|
||||
}
|
||||
|
||||
public bool Validate(bool validateHostsLength)
|
||||
{
|
||||
if (Equals("102.54.94.97", "rhino.acme.com", "source server") || Equals("38.25.63.10", "x.acme.com", "x client host"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ValidateAddressField() && ValidateHostsField(validateHostsLength);
|
||||
}
|
||||
|
||||
private bool Equals(string address, string hosts, string comment)
|
||||
{
|
||||
return string.Equals(Address, address, StringComparison.Ordinal)
|
||||
&& string.Equals(Hosts, hosts, StringComparison.Ordinal)
|
||||
&& string.Equals(Comment, comment, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/modules/Hosts/HostsUILib/Models/HostsData.cs
Normal file
37
src/modules/Hosts/HostsUILib/Models/HostsData.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace HostsUILib.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the parsed hosts file
|
||||
/// </summary>
|
||||
public class HostsData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the parsed entries
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<Entry> Entries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lines that couldn't be parsed
|
||||
/// </summary>
|
||||
public string AdditionalLines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether some entries been splitted
|
||||
/// </summary>
|
||||
public bool SplittedEntries { get; }
|
||||
|
||||
public HostsData(List<Entry> entries, string additionalLines, bool splittedEntries)
|
||||
{
|
||||
Entries = entries.AsReadOnly();
|
||||
AdditionalLines = additionalLines;
|
||||
SplittedEntries = splittedEntries;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 HostsUILib.Settings
|
||||
{
|
||||
public enum HostsAdditionalLinesPosition
|
||||
{
|
||||
Top = 0,
|
||||
Bottom = 1,
|
||||
}
|
||||
}
|
||||
12
src/modules/Hosts/HostsUILib/Settings/HostsEncoding.cs
Normal file
12
src/modules/Hosts/HostsUILib/Settings/HostsEncoding.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// 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 HostsUILib.Settings
|
||||
{
|
||||
public enum HostsEncoding
|
||||
{
|
||||
Utf8 = 0,
|
||||
Utf8Bom = 1,
|
||||
}
|
||||
}
|
||||
24
src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs
Normal file
24
src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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.Net;
|
||||
|
||||
namespace HostsUILib.Settings
|
||||
{
|
||||
public interface IUserSettings
|
||||
{
|
||||
public bool ShowStartupWarning { get; }
|
||||
|
||||
public bool LoopbackDuplicates { get; }
|
||||
|
||||
public HostsAdditionalLinesPosition AdditionalLinesPosition { get; }
|
||||
|
||||
public HostsEncoding Encoding { get; }
|
||||
|
||||
event EventHandler LoopbackDuplicatesChanged;
|
||||
|
||||
public delegate void OpenSettingsFunction();
|
||||
}
|
||||
}
|
||||
342
src/modules/Hosts/HostsUILib/Strings/en-us/Resources.resw
Normal file
342
src/modules/Hosts/HostsUILib/Strings/en-us/Resources.resw
Normal file
@@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Active.Header" xml:space="preserve">
|
||||
<value>Active</value>
|
||||
</data>
|
||||
<data name="ActiveToggle.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Active</value>
|
||||
</data>
|
||||
<data name="AddBtn" xml:space="preserve">
|
||||
<value>Add</value>
|
||||
</data>
|
||||
<data name="AddEntry.Text" xml:space="preserve">
|
||||
<value>New entry</value>
|
||||
</data>
|
||||
<data name="AddEntryBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>New entry</value>
|
||||
</data>
|
||||
<data name="AddEntryBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>New entry (Ctrl+N)</value>
|
||||
</data>
|
||||
<data name="AddEntryLink.Content" xml:space="preserve">
|
||||
<value>Add an entry</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Additional content</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Additional content</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesDialog.CloseButtonText" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesDialog.PrimaryButtonText" xml:space="preserve">
|
||||
<value>Save</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesDialog.Title" xml:space="preserve">
|
||||
<value>Additional content</value>
|
||||
</data>
|
||||
<data name="AddNewEntryDialog_Title" xml:space="preserve">
|
||||
<value>Add new entry</value>
|
||||
</data>
|
||||
<data name="Address.Header" xml:space="preserve">
|
||||
<value>Address</value>
|
||||
<comment>"Address" refers to the IP address of the entry</comment>
|
||||
</data>
|
||||
<data name="AddressFilter.Header" xml:space="preserve">
|
||||
<value>Address</value>
|
||||
<comment>"Address" refers to the IP address of the entry</comment>
|
||||
</data>
|
||||
<data name="ClearFiltersBtn.Content" xml:space="preserve">
|
||||
<value>Clear filters</value>
|
||||
</data>
|
||||
<data name="ClearFiltersBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Clear filters</value>
|
||||
</data>
|
||||
<data name="ClearFiltersLink.Content" xml:space="preserve">
|
||||
<value>Clear filters</value>
|
||||
</data>
|
||||
<data name="Comment.Header" xml:space="preserve">
|
||||
<value>Comment</value>
|
||||
<comment>"Comment" refers to the comment of the entry</comment>
|
||||
</data>
|
||||
<data name="CommentFilter.Header" xml:space="preserve">
|
||||
<value>Comment</value>
|
||||
<comment>"Comment" refers to the comment of the entry</comment>
|
||||
</data>
|
||||
<data name="Delete.Text" xml:space="preserve">
|
||||
<value>Delete</value>
|
||||
</data>
|
||||
<data name="DeleteDialog.CloseButtonText" xml:space="preserve">
|
||||
<value>No</value>
|
||||
</data>
|
||||
<data name="DeleteDialog.PrimaryButtonText" xml:space="preserve">
|
||||
<value>Yes</value>
|
||||
</data>
|
||||
<data name="DeleteDialogAreYouSure.Text" xml:space="preserve">
|
||||
<value>Are you sure you want to delete this entry?</value>
|
||||
</data>
|
||||
<data name="DeleteEntryBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Delete</value>
|
||||
</data>
|
||||
<data name="DeleteEntryBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Delete (Delete)</value>
|
||||
<comment>"Delete" between parentheses refers to the Delete keyboard key</comment>
|
||||
</data>
|
||||
<data name="Duplicate.Text" xml:space="preserve">
|
||||
<value>Duplicate</value>
|
||||
<comment>Refers to the action of duplicate an existing entry</comment>
|
||||
</data>
|
||||
<data name="DuplicateEntryIcon.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Duplicate entry</value>
|
||||
</data>
|
||||
<data name="Edit.Text" xml:space="preserve">
|
||||
<value>Edit</value>
|
||||
</data>
|
||||
<data name="EmptyFilterResults.Text" xml:space="preserve">
|
||||
<value>No filter results</value>
|
||||
</data>
|
||||
<data name="EmptyHosts.Text" xml:space="preserve">
|
||||
<value>No entries in the hosts file</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="Entries.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Entries</value>
|
||||
</data>
|
||||
<data name="EntryDialog.CloseButtonText" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="FileChanged.Message" xml:space="preserve">
|
||||
<value>Hosts file was modified externally.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="FileSaveError_FileInUse" xml:space="preserve">
|
||||
<value>The hosts file cannot be saved because it is being used by another process.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="FileSaveError_Generic" xml:space="preserve">
|
||||
<value>Unable to save the hosts file.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="FileSaveError_NotElevated" xml:space="preserve">
|
||||
<value>The hosts file cannot be saved because the program isn't running as administrator.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="FileSaveError_ReadOnly" xml:space="preserve">
|
||||
<value>The hosts file cannot be saved because it is read-only.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Filters</value>
|
||||
</data>
|
||||
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Filters</value>
|
||||
</data>
|
||||
<data name="Hosts.Header" xml:space="preserve">
|
||||
<value>Hosts</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="Hosts.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Separate multiple hosts by space (e.g. server server.local). Maximum 9 hosts per entry.</value>
|
||||
<comment>Do not localize "server" and "server.local"</comment>
|
||||
</data>
|
||||
<data name="HostsFilter.Header" xml:space="preserve">
|
||||
<value>Hosts</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="MakeWritable.Content" xml:space="preserve">
|
||||
<value>Make writable</value>
|
||||
</data>
|
||||
<data name="MoveDown.Text" xml:space="preserve">
|
||||
<value>Move down</value>
|
||||
</data>
|
||||
<data name="MoveUp.Text" xml:space="preserve">
|
||||
<value>Move up</value>
|
||||
</data>
|
||||
<data name="OpenHostsFileBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open hosts file</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="OpenHostsFileBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Open hosts file</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="Ping.Text" xml:space="preserve">
|
||||
<value>Ping</value>
|
||||
<comment>"Ping" refers to the command-line utility, do not loc</comment>
|
||||
</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">
|
||||
<value>Reload</value>
|
||||
</data>
|
||||
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="ShowOnlyDuplicates.Header" xml:space="preserve">
|
||||
<value>Show only duplicates</value>
|
||||
</data>
|
||||
<data name="TooManyHostsTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>Only 9 hosts per entry are supported. The affected entries have been split. This will take effect on next change.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="TooManyHostsTeachingTip.Title" xml:space="preserve">
|
||||
<value>Entries contain too many hosts</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="EntryAddressIsInvalidWarning.Text" xml:space="preserve">
|
||||
<value>Has to be a valid IPv4 or IPv6 address</value>
|
||||
</data>
|
||||
<data name="EntryHostsIsInvalidWarning.Text" xml:space="preserve">
|
||||
<value>Has to be one or more valid host names separated by spaces</value>
|
||||
</data>
|
||||
<data name="UpdateBtn" xml:space="preserve">
|
||||
<value>Update</value>
|
||||
</data>
|
||||
<data name="UpdateEntry_Title" xml:space="preserve">
|
||||
<value>Update the entry</value>
|
||||
</data>
|
||||
<data name="WarningDialog_AcceptBtn" xml:space="preserve">
|
||||
<value>Accept</value>
|
||||
</data>
|
||||
<data name="WarningDialog_QuitBtn" xml:space="preserve">
|
||||
<value>Quit</value>
|
||||
</data>
|
||||
<data name="WarningDialog_Text" xml:space="preserve">
|
||||
<value>Altering hosts file has direct real world impact of how this computer resolves domain names.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="WarningDialog_Title" xml:space="preserve">
|
||||
<value>Warning</value>
|
||||
</data>
|
||||
<data name="WindowAdminTitle" xml:space="preserve">
|
||||
<value>Administrator: Hosts File Editor</value>
|
||||
<comment>Title of the window when running as administrator. "Hosts File Editor" is the name of the utility. "Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="WindowTitle" xml:space="preserve">
|
||||
<value>Hosts File Editor</value>
|
||||
<comment>Title of the window when running as user. "Hosts File Editor" is the name of the utility. "Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
</root>
|
||||
460
src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs
Normal file
460
src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using HostsUILib.Exceptions;
|
||||
using HostsUILib.Helpers;
|
||||
using HostsUILib.Models;
|
||||
using HostsUILib.Settings;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using static HostsUILib.Settings.IUserSettings;
|
||||
|
||||
namespace HostsUILib.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;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _error;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _errorMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isReadOnly;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _fileChanged;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _addressFilter;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _hostsFilter;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _commentFilter;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _additionalLines;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _filtered;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showOnlyDuplicates;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showSplittedEntriesTooltip;
|
||||
|
||||
partial void OnShowOnlyDuplicatesChanged(bool value)
|
||||
{
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
private ObservableCollection<Entry> _entries;
|
||||
|
||||
public AdvancedCollectionView Entries { get; set; }
|
||||
|
||||
public int NextId => _entries?.Count > 0 ? _entries.Max(e => e.Id) + 1 : 0;
|
||||
|
||||
public IUserSettings UserSettings => _userSettings;
|
||||
|
||||
public static MainViewModel Instance { get; set; }
|
||||
|
||||
private OpenSettingsFunction _openSettingsFunction;
|
||||
|
||||
public MainViewModel(IHostsService hostService, IUserSettings userSettings, ILogger logger, OpenSettingsFunction openSettingsFunction)
|
||||
{
|
||||
_hostsService = hostService;
|
||||
_userSettings = userSettings;
|
||||
|
||||
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
||||
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
|
||||
|
||||
LoggerInstance.Logger = logger;
|
||||
_openSettingsFunction = openSettingsFunction;
|
||||
}
|
||||
|
||||
public void Add(Entry entry)
|
||||
{
|
||||
entry.PropertyChanged += Entry_PropertyChanged;
|
||||
_entries.Add(entry);
|
||||
|
||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||
}
|
||||
|
||||
public void Update(int index, Entry entry)
|
||||
{
|
||||
var existingEntry = Entries[index] as Entry;
|
||||
var oldAddress = existingEntry.Address;
|
||||
var oldHosts = existingEntry.SplittedHosts;
|
||||
|
||||
existingEntry.Address = entry.Address;
|
||||
existingEntry.Comment = entry.Comment;
|
||||
existingEntry.Hosts = entry.Hosts;
|
||||
existingEntry.Active = entry.Active;
|
||||
|
||||
FindDuplicates(oldAddress, oldHosts);
|
||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||
}
|
||||
|
||||
public void DeleteSelected()
|
||||
{
|
||||
var address = Selected.Address;
|
||||
var hosts = Selected.SplittedHosts;
|
||||
_entries.Remove(Selected);
|
||||
|
||||
FindDuplicates(address, hosts);
|
||||
}
|
||||
|
||||
public void UpdateAdditionalLines(string lines)
|
||||
{
|
||||
AdditionalLines = lines;
|
||||
_ = Task.Run(SaveAsync);
|
||||
}
|
||||
|
||||
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 DeleteEntry(Entry entry)
|
||||
{
|
||||
if (entry is not null)
|
||||
{
|
||||
var address = entry.Address;
|
||||
var hosts = entry.SplittedHosts;
|
||||
_entries.Remove(entry);
|
||||
|
||||
FindDuplicates(address, hosts);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void ReadHosts()
|
||||
{
|
||||
if (_readingHosts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
FileChanged = false;
|
||||
IsLoading = true;
|
||||
});
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
_readingHosts = true;
|
||||
var data = await _hostsService.ReadAsync();
|
||||
|
||||
await _dispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
ShowSplittedEntriesTooltip = data.SplittedEntries;
|
||||
AdditionalLines = data.AdditionalLines;
|
||||
_entries = new ObservableCollection<Entry>(data.Entries);
|
||||
|
||||
foreach (var e in _entries)
|
||||
{
|
||||
e.PropertyChanged += Entry_PropertyChanged;
|
||||
}
|
||||
|
||||
_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;
|
||||
|
||||
_tokenSource?.Cancel();
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
FindDuplicates(_tokenSource.Token);
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void ApplyFilters()
|
||||
{
|
||||
var expressions = new List<Expression<Func<object, bool>>>(4);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AddressFilter))
|
||||
{
|
||||
expressions.Add(e => ((Entry)e).Address.Contains(AddressFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
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;
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
public async Task PingSelectedAsync()
|
||||
{
|
||||
var selected = Selected;
|
||||
selected.Ping = null;
|
||||
selected.Pinging = true;
|
||||
selected.Ping = await _hostsService.PingAsync(Selected.Address);
|
||||
selected.Pinging = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenSettings()
|
||||
{
|
||||
_openSettingsFunction();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenHostsFile()
|
||||
{
|
||||
_hostsService.OpenHostsFile();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OverwriteHosts()
|
||||
{
|
||||
_hostsService.RemoveReadOnly();
|
||||
_ = Task.Run(SaveAsync);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
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 not trigger a file save
|
||||
if (e.PropertyName == nameof(Entry.Ping)
|
||||
|| e.PropertyName == nameof(Entry.Pinging)
|
||||
|| e.PropertyName == nameof(Entry.Duplicate))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(SaveAsync);
|
||||
}
|
||||
|
||||
private void Entries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
_ = Task.Run(SaveAsync);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
LoggerInstance.Logger.LogInfo("FindDuplicates cancelled");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FindDuplicates(string address, IEnumerable<string> hosts)
|
||||
{
|
||||
var entries = _entries.Where(e =>
|
||||
string.Equals(e.Address, address, StringComparison.OrdinalIgnoreCase)
|
||||
|| hosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any());
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
SetDuplicate(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDuplicate(Entry entry)
|
||||
{
|
||||
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
entry.Duplicate = false;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var duplicate = false;
|
||||
|
||||
/*
|
||||
* Duplicate are based on the following criteria:
|
||||
* Entries with the same type and at least one host in common
|
||||
* Entries with the same type and address, except when there is only one entry with less than 9 hosts for that type and address
|
||||
*/
|
||||
if (_entries.Any(e => e != entry
|
||||
&& e.Type == entry.Type
|
||||
&& entry.SplittedHosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any()))
|
||||
{
|
||||
duplicate = true;
|
||||
}
|
||||
else if (_entries.Any(e => e != entry
|
||||
&& e.Type == entry.Type
|
||||
&& string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
duplicate = entry.SplittedHosts.Length < Consts.MaxHostsCount
|
||||
&& _entries.Count(e => e.Type == entry.Type
|
||||
&& string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase)
|
||||
&& e.SplittedHosts.Length < Consts.MaxHostsCount) > 1;
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
entry.Duplicate = duplicate;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
bool error = true;
|
||||
string errorMessage = string.Empty;
|
||||
bool isReadOnly = false;
|
||||
|
||||
try
|
||||
{
|
||||
await _hostsService.WriteAsync(AdditionalLines, _entries);
|
||||
error = false;
|
||||
}
|
||||
catch (NotRunningElevatedException)
|
||||
{
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
errorMessage = resourceLoader.GetString("FileSaveError_NotElevated");
|
||||
}
|
||||
catch (ReadOnlyHostsException)
|
||||
{
|
||||
isReadOnly = true;
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
errorMessage = resourceLoader.GetString("FileSaveError_ReadOnly");
|
||||
}
|
||||
catch (IOException ex) when ((ex.HResult & 0x0000FFFF) == 32)
|
||||
{
|
||||
// There are some edge cases where a big hosts file is being locked by svchost.exe https://github.com/microsoft/PowerToys/issues/28066
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
errorMessage = resourceLoader.GetString("FileSaveError_FileInUse");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to save hosts file", ex);
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
errorMessage = resourceLoader.GetString("FileSaveError_Generic");
|
||||
}
|
||||
|
||||
await _dispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
Error = error;
|
||||
ErrorMessage = errorMessage;
|
||||
IsReadOnly = isReadOnly;
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_hostsService?.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user