Skip to content

Commit 0544698

Browse files
committed
Feature: Copy as in firewall/hostfileeditor
1 parent 0bf2a59 commit 0544698

13 files changed

Lines changed: 247 additions & 25 deletions

File tree

‎Source/NETworkManager.Localization/Resources/Strings.Designer.cs‎

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Source/NETworkManager.Localization/Resources/Strings.resx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4723,4 +4723,10 @@ Cloudflare may log your IP address and network information. See Cloudflare's pri
47234723
<data name="DeleteEntriesX" xml:space="preserve">
47244724
<value>Delete entries ({0})</value>
47254725
</data>
4726+
<data name="CopyEntry" xml:space="preserve">
4727+
<value>Copy entry</value>
4728+
</data>
4729+
<data name="CopyRule" xml:space="preserve">
4730+
<value>Copy rule</value>
4731+
</data>
47264732
</root>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace NETworkManager.Models;
2+
3+
/// <summary>
4+
/// Mode of an add/edit dialog.
5+
/// </summary>
6+
public enum EditMode
7+
{
8+
/// <summary>
9+
/// Add a new item with default values.
10+
/// </summary>
11+
Add,
12+
13+
/// <summary>
14+
/// Edit an existing item.
15+
/// </summary>
16+
Edit,
17+
18+
/// <summary>
19+
/// Add a new item, pre-filled with the values of an existing item.
20+
/// </summary>
21+
Copy
22+
}

‎Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -349,18 +349,22 @@ private static Dictionary<string, Queue<HostsFileEntry>> CreatePendingLookup(IEn
349349
/// Add a hosts file entry asynchronously.
350350
/// </summary>
351351
/// <param name="entry">Entry to add.</param>
352-
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was added successfully, otherwise an error result.</returns>"/>
353-
public static Task<HostsFileEntryModifyResult> AddEntryAsync(HostsFileEntry entry)
352+
/// <param name="insertAfter">Optional entry after which the new entry is inserted (e.g. the source entry
353+
/// when copying). If <see langword="null"/> or not found, the entry is added at the end of the hosts file.</param>
354+
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was added successfully, otherwise an error result.</returns>
355+
public static Task<HostsFileEntryModifyResult> AddEntryAsync(HostsFileEntry entry, HostsFileEntry insertAfter = null)
354356
{
355-
return Task.Run(() => AddEntry(entry));
357+
return Task.Run(() => AddEntry(entry, insertAfter));
356358
}
357359

358360
/// <summary>
359361
/// Add a hosts file entry.
360362
/// </summary>
361363
/// <param name="entry">Entry to add.</param>
362-
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was added successfully, otherwise an error result.</returns>"/>
363-
private static HostsFileEntryModifyResult AddEntry(HostsFileEntry entry)
364+
/// <param name="insertAfter">Optional entry after which the new entry is inserted. If <see langword="null"/>
365+
/// or not found, the entry is added at the end of the hosts file.</param>
366+
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was added successfully, otherwise an error result.</returns>
367+
private static HostsFileEntryModifyResult AddEntry(HostsFileEntry entry, HostsFileEntry insertAfter)
364368
{
365369
// Create a backup of the hosts file before making changes
366370
if (CreateBackup() == false)
@@ -382,8 +386,22 @@ private static HostsFileEntryModifyResult AddEntry(HostsFileEntry entry)
382386
return HostsFileEntryModifyResult.ReadError;
383387
}
384388

385-
Log.Debug($"AddEntry - Adding entry: {entry.Line}");
386-
hostsFileLines.Add(entry.Line);
389+
var index = insertAfter == null ? -1 : hostsFileLines.IndexOf(insertAfter.Line);
390+
391+
if (index >= 0)
392+
{
393+
Log.Debug($"AddEntry - Inserting entry after line {index + 1}: {entry.Line}");
394+
hostsFileLines.Insert(index + 1, entry.Line);
395+
}
396+
else
397+
{
398+
// The new entry doesn't depend on the source entry, so add it at the end if it was changed or removed
399+
if (insertAfter != null)
400+
Log.Debug($"AddEntry - Entry to insert after not found, adding at the end: {insertAfter.Line}");
401+
402+
Log.Debug($"AddEntry - Adding entry: {entry.Line}");
403+
hostsFileLines.Add(entry.Line);
404+
}
387405

388406
try
389407
{

‎Source/NETworkManager/ViewModels/FirewallRuleViewModel.cs‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using NETworkManager.Localization.Resources;
2+
using NETworkManager.Models;
13
using NETworkManager.Models.Firewall;
24
using NETworkManager.Utilities;
35
using System;
@@ -12,21 +14,23 @@ namespace NETworkManager.ViewModels;
1214
public class FirewallRuleViewModel : ViewModelBase
1315
{
1416
/// <summary>
15-
/// Creates a new instance of <see cref="FirewallRuleViewModel"/> for adding or
16-
/// editing a firewall rule.
17+
/// Creates a new instance of <see cref="FirewallRuleViewModel"/> for adding, editing or
18+
/// copying a firewall rule.
1719
/// </summary>
1820
/// <param name="okCommand">OK command to save the rule.</param>
1921
/// <param name="cancelHandler">Cancel command to discard changes.</param>
20-
/// <param name="entry">Existing rule to edit; <see langword="null"/> to add a new rule.</param>
22+
/// <param name="entry">Existing rule to edit or copy. Ignored when adding a new rule.</param>
23+
/// <param name="mode">Mode of the dialog (add, edit or copy).</param>
2124
public FirewallRuleViewModel(Action<FirewallRuleViewModel> okCommand,
22-
Action<FirewallRuleViewModel> cancelHandler, FirewallRule entry = null)
25+
Action<FirewallRuleViewModel> cancelHandler, FirewallRule entry = null, EditMode mode = EditMode.Add)
2326
{
2427
OKCommand = new RelayCommand(_ => okCommand(this));
2528
CancelCommand = new RelayCommand(_ => cancelHandler(this));
2629

27-
Entry = entry;
30+
Mode = mode;
31+
Entry = mode == EditMode.Add ? null : entry;
2832

29-
if (entry == null)
33+
if (Entry == null)
3034
{
3135
IsEnabled = true;
3236
Direction = FirewallRuleDirection.Inbound;
@@ -40,6 +44,10 @@ public FirewallRuleViewModel(Action<FirewallRuleViewModel> okCommand,
4044
else
4145
{
4246
Name = entry.Name;
47+
48+
if (mode == EditMode.Copy)
49+
Name += " - " + Strings.CopyNoun;
50+
4351
IsEnabled = entry.IsEnabled;
4452
Description = entry.Description ?? string.Empty;
4553
Direction = entry.Direction;
@@ -68,7 +76,13 @@ public FirewallRuleViewModel(Action<FirewallRuleViewModel> okCommand,
6876
public ICommand CancelCommand { get; }
6977

7078
/// <summary>
71-
/// The original firewall rule being edited, or <see langword="null"/> when adding a new rule.
79+
/// Mode of the dialog (add, edit or copy).
80+
/// </summary>
81+
public EditMode Mode { get; }
82+
83+
/// <summary>
84+
/// The original firewall rule being edited (or the source rule when copying),
85+
/// or <see langword="null"/> when adding a new rule.
7286
/// </summary>
7387
public FirewallRule Entry { get; }
7488

‎Source/NETworkManager/ViewModels/FirewallViewModel.cs‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using MahApps.Metro.Controls;
33
using MahApps.Metro.SimpleChildWindow;
44
using NETworkManager.Localization.Resources;
5+
using NETworkManager.Models;
56
using NETworkManager.Models.Export;
67
using NETworkManager.Models.Firewall;
78
using NETworkManager.Settings;
@@ -316,7 +317,7 @@ private async Task EditRule()
316317
{
317318
childWindow.IsOpen = false;
318319
ConfigurationManager.Current.IsChildWindowOpen = false;
319-
}, SelectedResult);
320+
}, SelectedResult, EditMode.Edit);
320321

321322
childWindow.Title = Strings.EditRule;
322323
childWindow.DataContext = childWindowViewModel;
@@ -326,6 +327,52 @@ private async Task EditRule()
326327
await Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
327328
}
328329

330+
/// <summary>
331+
/// Gets the command to copy the selected firewall rule.
332+
/// Only enabled when the application is running as administrator.
333+
/// </summary>
334+
public ICommand CopyAsRuleCommand => new RelayCommand(parameter => { _ = CopyAsRule(); }, _ => ModifyRule_CanExecute() && SelectedResult != null);
335+
336+
/// <summary>
337+
/// Opens the rule dialog pre-filled with the selected rule's properties (name suffixed with " - Copy").
338+
/// On confirmation, creates a new rule via PowerShell (the selected rule is not changed)
339+
/// and refreshes the rule list.
340+
/// </summary>
341+
private async Task CopyAsRule()
342+
{
343+
var childWindow = new FirewallRuleChildWindow();
344+
345+
var childWindowViewModel = new FirewallRuleViewModel(async instance =>
346+
{
347+
childWindow.IsOpen = false;
348+
ConfigurationManager.Current.IsChildWindowOpen = false;
349+
350+
try
351+
{
352+
await Firewall.AddRuleAsync(BuildRule(instance));
353+
await Refresh();
354+
}
355+
catch (Exception ex)
356+
{
357+
Log.Error("Error while copying firewall rule", ex);
358+
359+
StatusMessage = ex.Message;
360+
IsStatusMessageDisplayed = true;
361+
}
362+
}, _ =>
363+
{
364+
childWindow.IsOpen = false;
365+
ConfigurationManager.Current.IsChildWindowOpen = false;
366+
}, SelectedResult, EditMode.Copy);
367+
368+
childWindow.Title = Strings.CopyRule;
369+
childWindow.DataContext = childWindowViewModel;
370+
371+
ConfigurationManager.Current.IsChildWindowOpen = true;
372+
373+
await Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
374+
}
375+
329376
/// <summary>
330377
/// Gets the command to permanently delete the selected firewall rule.
331378
/// Only executable when a rule is selected and modification is allowed.

‎Source/NETworkManager/ViewModels/HostsFileEditorEntryViewModel.cs‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using NETworkManager.Localization.Resources;
2+
using NETworkManager.Models;
23
using NETworkManager.Models.HostsFileEditor;
34
using NETworkManager.Utilities;
45
using System;
@@ -20,28 +21,31 @@ public class HostsFileEditorEntryViewModel : ViewModelBase
2021
private readonly bool _isLoading;
2122

2223
/// <summary>
23-
/// Creates a new instance of <see cref="HostsFileEditorEntryViewModel" /> for adding or
24-
/// editing a hosts file entry.
24+
/// Creates a new instance of <see cref="HostsFileEditorEntryViewModel" /> for adding, editing or
25+
/// copying a hosts file entry.
2526
/// </summary>
2627
/// <param name="okCommand">OK command to save the entry.</param>
2728
/// <param name="cancelHandler">Cancel command to discard the entry.</param>
28-
/// <param name="entry">Entry to edit, if null a new entry will be created.</param>
29+
/// <param name="entry">Entry to edit or copy. Ignored when adding a new entry.</param>
30+
/// <param name="mode">Mode of the dialog (add, edit or copy).</param>
2931
public HostsFileEditorEntryViewModel(Action<HostsFileEditorEntryViewModel> okCommand,
30-
Action<HostsFileEditorEntryViewModel> cancelHandler, HostsFileEntry entry = null)
32+
Action<HostsFileEditorEntryViewModel> cancelHandler, HostsFileEntry entry = null,
33+
EditMode mode = EditMode.Add)
3134
{
3235
_isLoading = true;
3336

3437
OKCommand = new RelayCommand(_ => okCommand(this));
3538
CancelCommand = new RelayCommand(_ => cancelHandler(this));
3639

37-
Entry = entry;
40+
Mode = mode;
41+
Entry = mode == EditMode.Add ? null : entry;
3842

3943
// Add entry
4044
if (Entry == null)
4145
{
4246
IsEnabled = true;
4347
}
44-
// Edit entry
48+
// Edit or copy entry (the comment is not changed when copying, since it's written to the hosts file)
4549
else
4650
{
4751
IsEnabled = entry.IsEnabled;
@@ -68,6 +72,7 @@ public HostsFileEditorEntryViewModel(Action<HostsFileEditorEntryViewModel> okCom
6872
OKCommand = new RelayCommand(_ => okCommand(this));
6973
CancelCommand = new RelayCommand(_ => cancelHandler(this));
7074

75+
Mode = EditMode.Edit;
7176
Entries = entries;
7277
IsBulkEdit = true;
7378

@@ -93,7 +98,12 @@ public HostsFileEditorEntryViewModel(Action<HostsFileEditorEntryViewModel> okCom
9398
public ICommand CancelCommand { get; }
9499

95100
/// <summary>
96-
/// Gets the hosts file entry.
101+
/// Mode of the dialog (add, edit or copy).
102+
/// </summary>
103+
public EditMode Mode { get; }
104+
105+
/// <summary>
106+
/// Gets the hosts file entry (to edit, or the source entry when copying).
97107
/// </summary>
98108
public HostsFileEntry Entry { get; } = null;
99109

‎Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs‎

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using MahApps.Metro.Controls;
33
using MahApps.Metro.SimpleChildWindow;
44
using NETworkManager.Localization.Resources;
5+
using NETworkManager.Models;
56
using NETworkManager.Models.Export;
67
using NETworkManager.Models.HostsFileEditor;
78
using NETworkManager.Settings;
@@ -490,14 +491,68 @@ void CancelAction(HostsFileEditorEntryViewModel _)
490491
else
491492
{
492493
childWindow.Title = Strings.EditEntry;
493-
childWindow.DataContext = new HostsFileEditorEntryViewModel(OKAction, CancelAction, entries[0]);
494+
childWindow.DataContext = new HostsFileEditorEntryViewModel(OKAction, CancelAction, entries[0], EditMode.Edit);
494495
}
495496

496497
ConfigurationManager.Current.IsChildWindowOpen = true;
497498

498499
await Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
499500
}
500501

502+
/// <summary>
503+
/// Gets the command to copy the selected entry.
504+
/// </summary>
505+
public ICommand CopyAsEntryCommand =>
506+
new RelayCommand(parameter => { _ = CopyAsEntryAction(); }, ModifyEntry_CanExecute);
507+
508+
/// <summary>
509+
/// Action to copy the selected entry. Opens the entry dialog pre-filled with the values of the
510+
/// selected entry and adds the new entry directly below it.
511+
/// </summary>
512+
private async Task CopyAsEntryAction()
513+
{
514+
if (SelectedResult == null)
515+
return;
516+
517+
IsModifying = true;
518+
519+
var childWindow = new HostsFileEditorEntryChildWindow();
520+
521+
var childWindowViewModel = new HostsFileEditorEntryViewModel(async instance =>
522+
{
523+
childWindow.IsOpen = false;
524+
ConfigurationManager.Current.IsChildWindowOpen = false;
525+
526+
var result = await HostsFileEditor.AddEntryAsync(new HostsFileEntry
527+
(
528+
instance.IsEnabled,
529+
instance.IPAddress,
530+
instance.Hostname,
531+
instance.Comment
532+
), instance.Entry
533+
);
534+
535+
if (result != HostsFileEntryModifyResult.Success)
536+
await ShowErrorMessageAsync(result);
537+
538+
IsModifying = false;
539+
}, _ =>
540+
{
541+
childWindow.IsOpen = false;
542+
ConfigurationManager.Current.IsChildWindowOpen = false;
543+
544+
IsModifying = false;
545+
}, SelectedResult, EditMode.Copy);
546+
547+
childWindow.Title = Strings.CopyEntry;
548+
549+
childWindow.DataContext = childWindowViewModel;
550+
551+
ConfigurationManager.Current.IsChildWindowOpen = true;
552+
553+
await Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
554+
}
555+
501556
/// <summary>
502557
/// Gets the command to delete the selected entry.
503558
/// </summary>

0 commit comments

Comments
 (0)