2022-12-14 16:52:00 +01:00
|
|
|
// Copyright (c) Microsoft Corporation
|
2022-10-13 13:05:43 +02:00
|
|
|
// 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;
|
2022-12-14 16:52:00 +01:00
|
|
|
using System.Collections.Generic;
|
2022-10-13 13:05:43 +02:00
|
|
|
using System.Collections.ObjectModel;
|
2023-09-08 17:25:36 +02:00
|
|
|
using System.IO;
|
2022-10-13 13:05:43 +02:00
|
|
|
using System.Linq;
|
2023-02-27 19:11:57 +01:00
|
|
|
using System.Linq.Expressions;
|
2022-10-13 13:05:43 +02:00
|
|
|
using System.Threading.Tasks;
|
2024-09-16 16:09:43 -04:00
|
|
|
|
2022-10-13 13:05:43 +02:00
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
|
using CommunityToolkit.WinUI;
|
2023-09-14 18:41:31 +02:00
|
|
|
using CommunityToolkit.WinUI.Collections;
|
2024-04-26 19:41:44 +02:00
|
|
|
using HostsUILib.Exceptions;
|
|
|
|
|
using HostsUILib.Helpers;
|
|
|
|
|
using HostsUILib.Models;
|
|
|
|
|
using HostsUILib.Settings;
|
2022-10-13 13:05:43 +02:00
|
|
|
using Microsoft.UI.Dispatching;
|
2024-09-16 16:09:43 -04:00
|
|
|
|
2024-04-26 19:41:44 +02:00
|
|
|
using static HostsUILib.Settings.IUserSettings;
|
2022-10-13 13:05:43 +02:00
|
|
|
|
2024-04-26 19:41:44 +02:00
|
|
|
namespace HostsUILib.ViewModels
|
2022-10-13 13:05:43 +02:00
|
|
|
{
|
2024-06-03 11:26:05 +02:00
|
|
|
public partial class MainViewModel : ObservableObject
|
2022-10-13 13:05:43 +02:00
|
|
|
{
|
|
|
|
|
private readonly IHostsService _hostsService;
|
2023-02-27 19:11:57 +01:00
|
|
|
private readonly IUserSettings _userSettings;
|
2024-06-03 11:26:05 +02:00
|
|
|
private readonly IDuplicateService _duplicateService;
|
2022-10-13 13:05:43 +02:00
|
|
|
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
2023-02-27 19:11:57 +01:00
|
|
|
|
|
|
|
|
private bool _readingHosts;
|
2022-10-13 13:05:43 +02:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private Entry _selected;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _error;
|
|
|
|
|
|
2023-09-08 17:25:36 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _errorMessage;
|
|
|
|
|
|
2023-11-03 17:10:26 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _isReadOnly;
|
|
|
|
|
|
2022-10-13 13:05:43 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _fileChanged;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _addressFilter;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _hostsFilter;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _commentFilter;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _additionalLines;
|
|
|
|
|
|
2023-02-22 17:19:01 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _isLoading;
|
|
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _filtered;
|
2022-10-13 13:05:43 +02:00
|
|
|
|
2023-06-05 12:02:32 +02:00
|
|
|
[ObservableProperty]
|
2023-02-27 19:11:57 +01:00
|
|
|
private bool _showOnlyDuplicates;
|
2022-10-13 13:05:43 +02:00
|
|
|
|
2023-06-23 21:54:45 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _showSplittedEntriesTooltip;
|
|
|
|
|
|
2023-06-05 12:02:32 +02:00
|
|
|
partial void OnShowOnlyDuplicatesChanged(bool value)
|
2023-02-27 19:11:57 +01:00
|
|
|
{
|
2023-06-05 12:02:32 +02:00
|
|
|
ApplyFilters();
|
2023-02-27 19:11:57 +01:00
|
|
|
}
|
2022-10-13 13:05:43 +02:00
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
private ObservableCollection<Entry> _entries;
|
2022-10-13 13:05:43 +02:00
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
public AdvancedCollectionView Entries { get; set; }
|
2022-10-13 13:05:43 +02:00
|
|
|
|
2023-06-11 17:59:30 +02:00
|
|
|
public int NextId => _entries?.Count > 0 ? _entries.Max(e => e.Id) + 1 : 0;
|
2022-10-24 20:09:00 +02:00
|
|
|
|
2024-04-26 19:41:44 +02:00
|
|
|
public IUserSettings UserSettings => _userSettings;
|
|
|
|
|
|
|
|
|
|
public static MainViewModel Instance { get; set; }
|
|
|
|
|
|
|
|
|
|
private OpenSettingsFunction _openSettingsFunction;
|
|
|
|
|
|
2024-06-03 11:26:05 +02:00
|
|
|
public MainViewModel(
|
|
|
|
|
IHostsService hostService,
|
|
|
|
|
IUserSettings userSettings,
|
|
|
|
|
IDuplicateService duplicateService,
|
|
|
|
|
ILogger logger,
|
|
|
|
|
OpenSettingsFunction openSettingsFunction)
|
2022-10-13 13:05:43 +02:00
|
|
|
{
|
|
|
|
|
_hostsService = hostService;
|
2023-02-27 19:11:57 +01:00
|
|
|
_userSettings = userSettings;
|
2024-06-03 11:26:05 +02:00
|
|
|
_duplicateService = duplicateService;
|
2022-10-13 13:05:43 +02:00
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
|
|
|
|
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
|
2024-04-26 19:41:44 +02:00
|
|
|
|
|
|
|
|
LoggerInstance.Logger = logger;
|
|
|
|
|
_openSettingsFunction = openSettingsFunction;
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Add(Entry entry)
|
|
|
|
|
{
|
|
|
|
|
entry.PropertyChanged += Entry_PropertyChanged;
|
|
|
|
|
_entries.Add(entry);
|
2024-06-03 11:26:05 +02:00
|
|
|
_duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Update(int index, Entry entry)
|
|
|
|
|
{
|
2023-02-27 19:11:57 +01:00
|
|
|
var existingEntry = Entries[index] as Entry;
|
2022-12-14 16:52:00 +01:00
|
|
|
var oldAddress = existingEntry.Address;
|
|
|
|
|
var oldHosts = existingEntry.SplittedHosts;
|
|
|
|
|
|
2022-10-13 13:05:43 +02:00
|
|
|
existingEntry.Address = entry.Address;
|
|
|
|
|
existingEntry.Comment = entry.Comment;
|
|
|
|
|
existingEntry.Hosts = entry.Hosts;
|
|
|
|
|
existingEntry.Active = entry.Active;
|
2022-12-14 16:52:00 +01:00
|
|
|
|
2024-06-03 11:26:05 +02:00
|
|
|
_duplicateService.CheckDuplicates(oldAddress, oldHosts);
|
|
|
|
|
_duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void DeleteSelected()
|
|
|
|
|
{
|
2022-12-14 16:52:00 +01:00
|
|
|
var address = Selected.Address;
|
|
|
|
|
var hosts = Selected.SplittedHosts;
|
2022-10-13 13:05:43 +02:00
|
|
|
_entries.Remove(Selected);
|
2024-06-03 11:26:05 +02:00
|
|
|
_duplicateService.CheckDuplicates(address, hosts);
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void UpdateAdditionalLines(string lines)
|
|
|
|
|
{
|
2023-06-05 12:02:32 +02:00
|
|
|
AdditionalLines = lines;
|
2023-09-08 17:25:36 +02:00
|
|
|
_ = Task.Run(SaveAsync);
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 16:12:59 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
public void DeleteEntry(Entry entry)
|
|
|
|
|
{
|
|
|
|
|
if (entry is not null)
|
|
|
|
|
{
|
|
|
|
|
var address = entry.Address;
|
|
|
|
|
var hosts = entry.SplittedHosts;
|
|
|
|
|
_entries.Remove(entry);
|
2024-06-03 11:26:05 +02:00
|
|
|
_duplicateService.CheckDuplicates(address, hosts);
|
2024-02-14 16:12:59 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
[RelayCommand]
|
2022-10-13 13:05:43 +02:00
|
|
|
public void ReadHosts()
|
|
|
|
|
{
|
2023-02-27 19:11:57 +01:00
|
|
|
if (_readingHosts)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_dispatcherQueue.TryEnqueue(() =>
|
|
|
|
|
{
|
|
|
|
|
FileChanged = false;
|
|
|
|
|
IsLoading = true;
|
|
|
|
|
});
|
2022-10-13 13:05:43 +02:00
|
|
|
|
|
|
|
|
Task.Run(async () =>
|
|
|
|
|
{
|
2023-02-27 19:11:57 +01:00
|
|
|
_readingHosts = true;
|
2023-06-23 21:54:45 +02:00
|
|
|
var data = await _hostsService.ReadAsync();
|
2022-10-13 13:05:43 +02:00
|
|
|
|
|
|
|
|
await _dispatcherQueue.EnqueueAsync(() =>
|
|
|
|
|
{
|
2023-06-23 21:54:45 +02:00
|
|
|
ShowSplittedEntriesTooltip = data.SplittedEntries;
|
|
|
|
|
AdditionalLines = data.AdditionalLines;
|
|
|
|
|
_entries = new ObservableCollection<Entry>(data.Entries);
|
2022-10-13 13:05:43 +02:00
|
|
|
|
|
|
|
|
foreach (var e in _entries)
|
|
|
|
|
{
|
|
|
|
|
e.PropertyChanged += Entry_PropertyChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_entries.CollectionChanged += Entries_CollectionChanged;
|
2023-02-27 19:11:57 +01:00
|
|
|
Entries = new AdvancedCollectionView(_entries, true);
|
|
|
|
|
Entries.SortDescriptions.Add(new SortDescription(nameof(Entry.Id), SortDirection.Ascending));
|
|
|
|
|
ApplyFilters();
|
2022-12-14 16:52:00 +01:00
|
|
|
OnPropertyChanged(nameof(Entries));
|
2023-02-22 17:19:01 +01:00
|
|
|
IsLoading = false;
|
2022-10-13 13:05:43 +02:00
|
|
|
});
|
2023-02-27 19:11:57 +01:00
|
|
|
_readingHosts = false;
|
2023-02-22 17:19:01 +01:00
|
|
|
|
2024-06-03 11:26:05 +02:00
|
|
|
_duplicateService.Initialize(_entries);
|
2022-10-13 13:05:43 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
[RelayCommand]
|
2022-10-13 13:05:43 +02:00
|
|
|
public void ApplyFilters()
|
|
|
|
|
{
|
2023-02-27 19:11:57 +01:00
|
|
|
var expressions = new List<Expression<Func<object, bool>>>(4);
|
|
|
|
|
|
2023-06-05 12:02:32 +02:00
|
|
|
if (!string.IsNullOrWhiteSpace(AddressFilter))
|
2022-10-13 13:05:43 +02:00
|
|
|
{
|
2023-06-05 12:02:32 +02:00
|
|
|
expressions.Add(e => ((Entry)e).Address.Contains(AddressFilter, StringComparison.OrdinalIgnoreCase));
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
2022-12-14 16:52:00 +01:00
|
|
|
|
2023-06-05 12:02:32 +02:00
|
|
|
if (!string.IsNullOrWhiteSpace(HostsFilter))
|
2023-02-27 19:11:57 +01:00
|
|
|
{
|
2023-06-05 12:02:32 +02:00
|
|
|
expressions.Add(e => ((Entry)e).Hosts.Contains(HostsFilter, StringComparison.OrdinalIgnoreCase));
|
2023-02-27 19:11:57 +01:00
|
|
|
}
|
2022-12-14 16:52:00 +01:00
|
|
|
|
2023-06-05 12:02:32 +02:00
|
|
|
if (!string.IsNullOrWhiteSpace(CommentFilter))
|
2023-02-27 19:11:57 +01:00
|
|
|
{
|
2023-06-05 12:02:32 +02:00
|
|
|
expressions.Add(e => ((Entry)e).Comment.Contains(CommentFilter, StringComparison.OrdinalIgnoreCase));
|
2023-02-27 19:11:57 +01:00
|
|
|
}
|
|
|
|
|
|
2023-06-05 12:02:32 +02:00
|
|
|
if (ShowOnlyDuplicates)
|
2023-02-27 19:11:57 +01:00
|
|
|
{
|
|
|
|
|
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();
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
[RelayCommand]
|
2022-10-13 13:05:43 +02:00
|
|
|
public void ClearFilters()
|
|
|
|
|
{
|
|
|
|
|
AddressFilter = null;
|
|
|
|
|
HostsFilter = null;
|
|
|
|
|
CommentFilter = null;
|
2022-12-14 16:52:00 +01:00
|
|
|
ShowOnlyDuplicates = false;
|
2023-06-11 17:59:30 +02:00
|
|
|
ApplyFilters();
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task PingSelectedAsync()
|
|
|
|
|
{
|
2023-06-05 12:02:32 +02:00
|
|
|
var selected = Selected;
|
2022-10-13 13:05:43 +02:00
|
|
|
selected.Ping = null;
|
|
|
|
|
selected.Pinging = true;
|
2023-06-05 12:02:32 +02:00
|
|
|
selected.Ping = await _hostsService.PingAsync(Selected.Address);
|
2022-10-13 13:05:43 +02:00
|
|
|
selected.Pinging = false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
[RelayCommand]
|
2022-10-13 13:05:43 +02:00
|
|
|
public void OpenSettings()
|
|
|
|
|
{
|
2024-04-26 19:41:44 +02:00
|
|
|
_openSettingsFunction();
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-27 19:11:57 +01:00
|
|
|
[RelayCommand]
|
2022-10-24 20:09:00 +02:00
|
|
|
public void OpenHostsFile()
|
|
|
|
|
{
|
|
|
|
|
_hostsService.OpenHostsFile();
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-03 17:10:26 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
public void OverwriteHosts()
|
|
|
|
|
{
|
2024-08-16 20:36:26 +02:00
|
|
|
_hostsService.RemoveReadOnlyAttribute();
|
2023-11-03 17:10:26 +01:00
|
|
|
_ = Task.Run(SaveAsync);
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-13 13:05:43 +02:00
|
|
|
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
|
|
|
{
|
2023-02-27 19:11:57 +01:00
|
|
|
if (Filtered && (e.PropertyName == nameof(Entry.Hosts)
|
|
|
|
|
|| e.PropertyName == nameof(Entry.Address)
|
|
|
|
|
|| e.PropertyName == nameof(Entry.Comment)
|
|
|
|
|
|| e.PropertyName == nameof(Entry.Duplicate)))
|
|
|
|
|
{
|
|
|
|
|
Entries.RefreshFilter();
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 16:06:01 +01:00
|
|
|
// Ping and duplicate should not trigger a file save
|
2022-12-14 16:52:00 +01:00
|
|
|
if (e.PropertyName == nameof(Entry.Ping)
|
|
|
|
|
|| e.PropertyName == nameof(Entry.Pinging)
|
|
|
|
|
|| e.PropertyName == nameof(Entry.Duplicate))
|
2022-10-13 13:05:43 +02:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-08 17:25:36 +02:00
|
|
|
_ = Task.Run(SaveAsync);
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Entries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
|
|
|
|
{
|
2023-09-08 17:25:36 +02:00
|
|
|
_ = Task.Run(SaveAsync);
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-08 17:25:36 +02:00
|
|
|
private async Task SaveAsync()
|
|
|
|
|
{
|
|
|
|
|
bool error = true;
|
|
|
|
|
string errorMessage = string.Empty;
|
2023-11-03 17:10:26 +01:00
|
|
|
bool isReadOnly = false;
|
2023-09-08 17:25:36 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await _hostsService.WriteAsync(AdditionalLines, _entries);
|
|
|
|
|
error = false;
|
|
|
|
|
}
|
|
|
|
|
catch (NotRunningElevatedException)
|
|
|
|
|
{
|
|
|
|
|
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
|
|
|
|
errorMessage = resourceLoader.GetString("FileSaveError_NotElevated");
|
|
|
|
|
}
|
2023-11-03 17:10:26 +01:00
|
|
|
catch (ReadOnlyHostsException)
|
|
|
|
|
{
|
|
|
|
|
isReadOnly = true;
|
|
|
|
|
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
|
|
|
|
errorMessage = resourceLoader.GetString("FileSaveError_ReadOnly");
|
|
|
|
|
}
|
2023-09-08 17:25:36 +02:00
|
|
|
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)
|
|
|
|
|
{
|
2024-04-26 19:41:44 +02:00
|
|
|
LoggerInstance.Logger.LogError("Failed to save hosts file", ex);
|
2023-09-08 17:25:36 +02:00
|
|
|
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
|
|
|
|
errorMessage = resourceLoader.GetString("FileSaveError_Generic");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await _dispatcherQueue.EnqueueAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
Error = error;
|
|
|
|
|
ErrorMessage = errorMessage;
|
2023-11-03 17:10:26 +01:00
|
|
|
IsReadOnly = isReadOnly;
|
2023-09-08 17:25:36 +02:00
|
|
|
});
|
|
|
|
|
}
|
2022-10-13 13:05:43 +02:00
|
|
|
}
|
|
|
|
|
}
|