From a6857a06271044d40dac8e51bde16c1128da7f93 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 6 Jul 2025 02:52:27 +0200 Subject: [PATCH 1/2] Feature: HostsFileEditor delete entry added --- .../Resources/Strings.Designer.cs | 11 ++ .../Resources/Strings.resx | 5 + .../HostsFileEditor/HostsFileEditor.cs | 132 ++++++++++++++++-- .../HostsFileEntryModifyResult.cs | 28 ++++ .../ViewModels/HostsFileEditorViewModel.cs | 57 +++++++- .../ViewModels/SNTPLookupSettingsViewModel.cs | 17 ++- .../Views/ExportChildWindow.xaml | 2 +- .../Views/GroupChildWindow.xaml | 2 +- .../Views/OKCancelInfoMessageChildWindow.xaml | 7 +- .../Views/ProfileChildWindow.xaml | 2 +- 10 files changed, 232 insertions(+), 31 deletions(-) create mode 100644 Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 0b8f1f0128..1b033dfbba 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -2591,6 +2591,17 @@ public static string DeleteGroupMessage { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die The selected entry is permanently deleted: + /// + ///{0} {1} {2} ähnelt. + /// + public static string DeleteHostsFileEntryMessage { + get { + return ResourceManager.GetString("DeleteHostsFileEntryMessage", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Delete OID profile ähnelt. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index f8293f0385..11315b1d1e 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3933,4 +3933,9 @@ Right-click for more options. Entries + + The selected entry is permanently deleted: + +{0} {1} {2} + \ No newline at end of file diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs index a692908df7..34ae2f28c6 100644 --- a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs +++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs @@ -164,27 +164,52 @@ private static IEnumerable GetHostsFileEntries() return entries; } - public static Task EnableEntryAsync(HostsFileEntry entry) + /// + /// Enable a hosts file entry asynchronously. + /// + /// Entry to enable. + /// if the entry was enabled successfully, otherwise an error result. + public static Task EnableEntryAsync(HostsFileEntry entry) { return Task.Run(() => EnableEntry(entry)); } - private static bool EnableEntry(HostsFileEntry entry) + /// + /// Enable a hosts file entry. + /// + /// Entry to enable. + /// if the entry was enabled successfully, otherwise an error result. + private static HostsFileEntryModifyResult EnableEntry(HostsFileEntry entry) { // Create a backup of the hosts file before making changes if (CreateBackup() == false) { Log.Error("EnableEntry - Failed to create backup before enabling entry."); - return false; + return HostsFileEntryModifyResult.BackupError; } // Replace the entry in the hosts file var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList(); + bool entryFound = false; + for (var i = 0; i < hostsFileLines.Count; i++) { if (hostsFileLines[i] == entry.Line) + { + entryFound = true; + hostsFileLines[i] = entry.Line.TrimStart('#', ' '); + + break; + } + } + + if (!entryFound) + { + Log.Warn($"EnableEntry - Entry not found in hosts file: {entry.Line}"); + + return HostsFileEntryModifyResult.NotFound; } try @@ -195,34 +220,57 @@ private static bool EnableEntry(HostsFileEntry entry) catch (Exception ex) { Log.Error($"EnableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex); - - return false; + return HostsFileEntryModifyResult.WriteError; } - return true; + return HostsFileEntryModifyResult.Success; } - public static Task DisableEntryAsync(HostsFileEntry entry) + /// + /// Disable a hosts file entry asynchronously. + /// + /// Entry to disable. + /// if the entry was enabled successfully, otherwise an error result. + public static Task DisableEntryAsync(HostsFileEntry entry) { return Task.Run(() => DisableEntry(entry)); } - private static bool DisableEntry(HostsFileEntry entry) + /// + /// Disable a hosts file entry. + /// + /// Entry to disable. + /// if the entry was enabled successfully, otherwise an error result. + private static HostsFileEntryModifyResult DisableEntry(HostsFileEntry entry) { // Create a backup of the hosts file before making changes if (CreateBackup() == false) { Log.Error("DisableEntry - Failed to create backup before disabling entry."); - return false; + return HostsFileEntryModifyResult.BackupError; } // Replace the entry in the hosts file var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList(); + bool entryFound = false; + for (var i = 0; i < hostsFileLines.Count; i++) { if (hostsFileLines[i] == entry.Line) + { + entryFound = true; + hostsFileLines[i] = "# " + entry.Line; + + break; + } + } + + if (!entryFound) + { + Log.Warn($"DisableEntry - Entry not found in hosts file: {entry.Line}"); + return HostsFileEntryModifyResult.NotFound; } try @@ -233,15 +281,75 @@ private static bool DisableEntry(HostsFileEntry entry) catch (Exception ex) { Log.Error($"DisableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex); + return HostsFileEntryModifyResult.WriteError; + } - return false; + return HostsFileEntryModifyResult.Success; + } + + /// + /// Delete a hosts file entry asynchronously. + /// + /// Entry to delete."/> + /// if the entry was enabled successfully, otherwise an error result.s + public static Task DeleteEntryAsync(HostsFileEntry entry) + { + return Task.Run(() => DeleteEntry(entry)); + } + + /// + /// Delete a hosts file entry. + /// + /// Entry to delete."/> + /// if the entry was enabled successfully, otherwise an error result. + private static HostsFileEntryModifyResult DeleteEntry(HostsFileEntry entry) + { + // Create a backup of the hosts file before making changes + if (CreateBackup() == false) + { + Log.Error("DeleteEntry - Failed to create backup before deleting entry."); + return HostsFileEntryModifyResult.BackupError; } - return true; + // Remove the entry from the hosts file + var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList(); + + bool entryFound = false; + + for (var i = 0; i < hostsFileLines.Count; i++) + { + if (hostsFileLines[i] == entry.Line) + { + entryFound = true; + + hostsFileLines.RemoveAt(i); + + break; + } + } + + if (!entryFound) + { + Log.Warn($"DeleteEntry - Entry not found in hosts file: {entry.Line}"); + return HostsFileEntryModifyResult.NotFound; + } + + try + { + Log.Debug($"DeleteEntry - Writing changes to hosts file: {HostsFilePath}"); + File.WriteAllLines(HostsFilePath, hostsFileLines); + } + catch (Exception ex) + { + Log.Error($"DeleteEntry - Failed to write changes to hosts file: {HostsFilePath}", ex); + return HostsFileEntryModifyResult.WriteError; + } + OnHostsFileChanged(); + return HostsFileEntryModifyResult.Success; } /// - /// Create a daily backup of the hosts file (before making a change). + /// Create a "daily" backup of the hosts file (before making a change). /// private static bool CreateBackup() { diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs new file mode 100644 index 0000000000..365ee39b86 --- /dev/null +++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs @@ -0,0 +1,28 @@ +namespace NETworkManager.Models.HostsFileEditor +{ + /// + /// Represents the result of an attempt to modify a hosts file entry. + /// + public enum HostsFileEntryModifyResult + { + /// + /// The entry was modified successfully and the hosts file was updated. + /// + Success, + + /// + /// The entry was not found in the hosts file. + /// + NotFound, + + /// + /// An error occurred while writing to the hosts file. + /// + WriteError, + + /// + /// An error occurred while backing up the hosts file. + /// + BackupError, + } +} diff --git a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs index 9baa98157f..30cdb00ca6 100644 --- a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs +++ b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs @@ -93,6 +93,21 @@ public IList SelectedResults } } + private bool _isModifying; + + public bool IsModifying + { + get => _isModifying; + set + { + if (value == _isModifying) + return; + + _isModifying = value; + OnPropertyChanged(); + } + } + private bool _isRefreshing; public bool IsRefreshing @@ -192,7 +207,8 @@ private bool Refresh_CanExecute(object parameter) return Application.Current.MainWindow != null && !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen && !ConfigurationManager.Current.IsChildWindowOpen && - !IsRefreshing; + !IsRefreshing && + !IsModifying; } private async Task RefreshAction() @@ -253,14 +269,22 @@ await _dialogCoordinator.ShowMessageAsync(this, Strings.Error, private async Task EnableEntryAction() { + IsModifying = true; + await HostsFileEditor.EnableEntryAsync(SelectedResult); + + IsModifying = false; } public ICommand DisableEntryCommand => new RelayCommand(_ => DisableEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute); private async Task DisableEntryAction() { + IsModifying = true; + await HostsFileEditor.DisableEntryAsync(SelectedResult); + + IsModifying = false; } public ICommand AddEntryCommand => new RelayCommand(_ => AddEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute); @@ -274,7 +298,33 @@ private async Task AddEntryAction() private async Task DeleteEntryAction() { - MessageBox.Show("Delete entry action is not implemented yet.", "Delete Entry", MessageBoxButton.OK, MessageBoxImage.Information); + IsModifying = true; + + var childWindow = new OKCancelInfoMessageChildWindow(); + + var childWindowViewModel = new OKCancelInfoMessageViewModel(async _ => + { + childWindow.IsOpen = false; + ConfigurationManager.Current.IsChildWindowOpen = false; + + await HostsFileEditor.DeleteEntryAsync(SelectedResult); + + IsModifying = false; + }, _ => + { + childWindow.IsOpen = false; + ConfigurationManager.Current.IsChildWindowOpen = false; + + IsModifying = false; + }, string.Format(Strings.DeleteHostsFileEntryMessage, SelectedResult.IPAddress, SelectedResult.Hostname, string.IsNullOrEmpty(SelectedResult.Comment) ? "" : $"# {SelectedResult.Comment}")); + + childWindow.Title = Strings.DeleteEntry; + + childWindow.DataContext = childWindowViewModel; + + ConfigurationManager.Current.IsChildWindowOpen = true; + + await (Application.Current.MainWindow as MainWindow).ShowChildWindowAsync(childWindow); } public ICommand EditEntryCommand => new RelayCommand(_ => EditEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute); @@ -290,7 +340,8 @@ private bool ModifyEntry_CanExecute(object obj) Application.Current.MainWindow != null && !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen && !ConfigurationManager.Current.IsChildWindowOpen && - !IsRefreshing; + !IsRefreshing && + !IsModifying; } public ICommand RestartAsAdminCommand => new RelayCommand(_ => RestartAsAdminAction().ConfigureAwait(false)); diff --git a/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs index 46a8e4ed4e..8dc6df77d3 100644 --- a/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Data; -using System.Windows.Input; -using MahApps.Metro.Controls.Dialogs; +using MahApps.Metro.Controls.Dialogs; using MahApps.Metro.SimpleChildWindow; using NETworkManager.Localization.Resources; using NETworkManager.Models.Network; using NETworkManager.Settings; using NETworkManager.Utilities; using NETworkManager.Views; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; +using System.Windows.Input; namespace NETworkManager.ViewModels; @@ -189,7 +189,6 @@ private Task DeleteServer() { var childWindow = new OKCancelInfoMessageChildWindow(); - var childWindowViewModel = new OKCancelInfoMessageViewModel(_ => { childWindow.IsOpen = false; diff --git a/Source/NETworkManager/Views/ExportChildWindow.xaml b/Source/NETworkManager/Views/ExportChildWindow.xaml index 3a9eb50508..896de4c21f 100644 --- a/Source/NETworkManager/Views/ExportChildWindow.xaml +++ b/Source/NETworkManager/Views/ExportChildWindow.xaml @@ -30,7 +30,7 @@ - + diff --git a/Source/NETworkManager/Views/GroupChildWindow.xaml b/Source/NETworkManager/Views/GroupChildWindow.xaml index 4328ceb7dd..633e30aff4 100644 --- a/Source/NETworkManager/Views/GroupChildWindow.xaml +++ b/Source/NETworkManager/Views/GroupChildWindow.xaml @@ -43,7 +43,7 @@ - + diff --git a/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml b/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml index 1fd3d8456a..1453bdcd92 100644 --- a/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml +++ b/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml @@ -17,12 +17,12 @@ AllowMove="True" TitleForeground="{DynamicResource MahApps.Brushes.Gray3}" CloseByEscape="False" - OverlayBrush="{DynamicResource ResourceKey=MahApps.Brushes.Gray.SemiTransparent}" + OverlayBrush="{DynamicResource ResourceKey=MahApps.Brushes.Gray.SemiTransparent}" mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:OKCancelInfoMessageViewModel}"> - + + Style="{StaticResource ResourceKey=WrapTextBlock}"/> diff --git a/Source/NETworkManager/Views/ProfileChildWindow.xaml b/Source/NETworkManager/Views/ProfileChildWindow.xaml index 6cffd4ac6b..5b306f6177 100644 --- a/Source/NETworkManager/Views/ProfileChildWindow.xaml +++ b/Source/NETworkManager/Views/ProfileChildWindow.xaml @@ -46,7 +46,7 @@ - + From 22437dc639b25ba0b8aa0c29a759b29b56f00525 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 6 Jul 2025 02:55:17 +0200 Subject: [PATCH 2/2] Docs: #3094 --- Website/docs/changelog/next-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 102ec081a5..630157d855 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -23,7 +23,7 @@ Release date: **xx.xx.2025** **Hosts File Editor** -- New feature to display (and edit) the `hosts` file. (See [documentation](https://borntoberoot.net/NETworkManager/docs/application/hosts-file-editor) for more details) [#3012](https://github.com/BornToBeRoot/NETworkManager/pull/3012) [#3092](https://github.com/BornToBeRoot/NETworkManager/pull/3092) +- New feature to display (and edit) the `hosts` file. (See [documentation](https://borntoberoot.net/NETworkManager/docs/application/hosts-file-editor) for more details) [#3012](https://github.com/BornToBeRoot/NETworkManager/pull/3012) [#3092](https://github.com/BornToBeRoot/NETworkManager/pull/3092) [#3094](https://github.com/BornToBeRoot/NETworkManager/pull/3094) :::info