From f133c740e191f95104456289fa266ce62899904a Mon Sep 17 00:00:00 2001 From: Kevin Kai Berthold Date: Wed, 24 Sep 2025 22:10:04 +0200 Subject: [PATCH] migration --- Program.cs | 12 - discovery.sln | 60 +++ dotnetcore.csproj | 8 - src/Discovery.Avalonia/App.axaml | 16 + src/Discovery.Avalonia/App.axaml.cs | 30 ++ src/Discovery.Avalonia/Assets/icon.ico | Bin 0 -> 23462 bytes .../Converters/MoveConverters.cs | 47 +++ .../Discovery.Avalonia.csproj | 53 +++ .../Models/TargetViewModel.cs | 42 +++ .../ViewModels/MainViewModel.cs | 354 ++++++++++++++++++ .../ViewModels/OptionViewModel.cs | 92 +++++ .../ViewModels/ViewModelBase.cs | 7 + src/Discovery.Avalonia/Views/MainView.axaml | 280 ++++++++++++++ .../Views/MainView.axaml.cs | 11 + .../Windows/MainWindow.axaml | 15 + .../Windows/MainWindow.axaml.cs | 32 ++ .../Windows/OptionWindow.axaml | 177 +++++++++ .../Windows/OptionWindow.axaml.cs | 11 + src/Discovery.Core/Discovery.Core.csproj | 35 ++ src/Discovery.Core/Helpers/IpHelper.cs | 24 ++ src/Discovery.Core/Models/NetworkInfo.cs | 5 + src/Discovery.Core/Models/ScannerEvents.cs | 6 + src/Discovery.Core/Models/ScannerOptions.cs | 14 + src/Discovery.Core/Models/ScannerState.cs | 15 + .../Models/SortableObservableCollection.cs | 190 ++++++++++ src/Discovery.Core/Models/Target.cs | 102 +++++ src/Discovery.Core/Models/Vendor.cs | 15 + src/Discovery.Core/Services/Exporter.cs | 113 ++++++ src/Discovery.Core/Services/Scanner.cs | 295 +++++++++++++++ src/Discovery.Core/Services/WinArpInterop.cs | 106 ++++++ src/Discovery.Core/vendors.json | 1 + .../Discovery.Desktop.csproj | 47 +++ src/Discovery.Desktop/Program.cs | 18 + .../Properties/PublishProfiles/aot.pubxml | 21 ++ .../Properties/PublishProfiles/gc.pubxml | 20 + src/Discovery.Desktop/app.manifest | 18 + src/Discovery.Desktop/icon.ico | Bin 0 -> 23462 bytes test/Discovery.Loader.Avalonia/App.axaml | 9 + test/Discovery.Loader.Avalonia/App.axaml.cs | 135 +++++++ .../Discovery.Loader.Avalonia/Assets/icon.ico | Bin 0 -> 23462 bytes .../Discovery.Loader.Avalonia.csproj | 27 ++ .../Models/Update.cs | 18 + .../Services/UpdateService.cs | 167 +++++++++ .../ViewModels/MainViewModel.cs | 105 ++++++ .../ViewModels/ViewModelBase.cs | 7 + .../Views/MainView.axaml | 33 ++ .../Views/MainView.axaml.cs | 11 + .../Windows/MainWindow.axaml | 20 + .../Windows/MainWindow.axaml.cs | 14 + .../Discovery.Loader.Desktop.csproj | 33 ++ test/Discovery.Loader/Program.cs | 18 + .../Properties/PublishProfiles/aot.pubxml | 21 ++ .../Properties/PublishProfiles/jit.pubxml | 20 + test/Discovery.Loader/app.manifest | 18 + test/Discovery.Loader/icon.ico | Bin 0 -> 23462 bytes 55 files changed, 2928 insertions(+), 20 deletions(-) delete mode 100644 Program.cs create mode 100644 discovery.sln delete mode 100644 dotnetcore.csproj create mode 100644 src/Discovery.Avalonia/App.axaml create mode 100644 src/Discovery.Avalonia/App.axaml.cs create mode 100644 src/Discovery.Avalonia/Assets/icon.ico create mode 100644 src/Discovery.Avalonia/Converters/MoveConverters.cs create mode 100644 src/Discovery.Avalonia/Discovery.Avalonia.csproj create mode 100644 src/Discovery.Avalonia/Models/TargetViewModel.cs create mode 100644 src/Discovery.Avalonia/ViewModels/MainViewModel.cs create mode 100644 src/Discovery.Avalonia/ViewModels/OptionViewModel.cs create mode 100644 src/Discovery.Avalonia/ViewModels/ViewModelBase.cs create mode 100644 src/Discovery.Avalonia/Views/MainView.axaml create mode 100644 src/Discovery.Avalonia/Views/MainView.axaml.cs create mode 100644 src/Discovery.Avalonia/Windows/MainWindow.axaml create mode 100644 src/Discovery.Avalonia/Windows/MainWindow.axaml.cs create mode 100644 src/Discovery.Avalonia/Windows/OptionWindow.axaml create mode 100644 src/Discovery.Avalonia/Windows/OptionWindow.axaml.cs create mode 100644 src/Discovery.Core/Discovery.Core.csproj create mode 100644 src/Discovery.Core/Helpers/IpHelper.cs create mode 100644 src/Discovery.Core/Models/NetworkInfo.cs create mode 100644 src/Discovery.Core/Models/ScannerEvents.cs create mode 100644 src/Discovery.Core/Models/ScannerOptions.cs create mode 100644 src/Discovery.Core/Models/ScannerState.cs create mode 100644 src/Discovery.Core/Models/SortableObservableCollection.cs create mode 100644 src/Discovery.Core/Models/Target.cs create mode 100644 src/Discovery.Core/Models/Vendor.cs create mode 100644 src/Discovery.Core/Services/Exporter.cs create mode 100644 src/Discovery.Core/Services/Scanner.cs create mode 100644 src/Discovery.Core/Services/WinArpInterop.cs create mode 100644 src/Discovery.Core/vendors.json create mode 100644 src/Discovery.Desktop/Discovery.Desktop.csproj create mode 100644 src/Discovery.Desktop/Program.cs create mode 100644 src/Discovery.Desktop/Properties/PublishProfiles/aot.pubxml create mode 100644 src/Discovery.Desktop/Properties/PublishProfiles/gc.pubxml create mode 100644 src/Discovery.Desktop/app.manifest create mode 100644 src/Discovery.Desktop/icon.ico create mode 100644 test/Discovery.Loader.Avalonia/App.axaml create mode 100644 test/Discovery.Loader.Avalonia/App.axaml.cs create mode 100644 test/Discovery.Loader.Avalonia/Assets/icon.ico create mode 100644 test/Discovery.Loader.Avalonia/Discovery.Loader.Avalonia.csproj create mode 100644 test/Discovery.Loader.Avalonia/Models/Update.cs create mode 100644 test/Discovery.Loader.Avalonia/Services/UpdateService.cs create mode 100644 test/Discovery.Loader.Avalonia/ViewModels/MainViewModel.cs create mode 100644 test/Discovery.Loader.Avalonia/ViewModels/ViewModelBase.cs create mode 100644 test/Discovery.Loader.Avalonia/Views/MainView.axaml create mode 100644 test/Discovery.Loader.Avalonia/Views/MainView.axaml.cs create mode 100644 test/Discovery.Loader.Avalonia/Windows/MainWindow.axaml create mode 100644 test/Discovery.Loader.Avalonia/Windows/MainWindow.axaml.cs create mode 100644 test/Discovery.Loader/Discovery.Loader.Desktop.csproj create mode 100644 test/Discovery.Loader/Program.cs create mode 100644 test/Discovery.Loader/Properties/PublishProfiles/aot.pubxml create mode 100644 test/Discovery.Loader/Properties/PublishProfiles/jit.pubxml create mode 100644 test/Discovery.Loader/app.manifest create mode 100644 test/Discovery.Loader/icon.ico diff --git a/Program.cs b/Program.cs deleted file mode 100644 index a3388e0..0000000 --- a/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnetcore -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} diff --git a/discovery.sln b/discovery.sln new file mode 100644 index 0000000..faec255 --- /dev/null +++ b/discovery.sln @@ -0,0 +1,60 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36518.9 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discovery.Core", "src\Discovery.Core\Discovery.Core.csproj", "{69E5A763-1B0B-A8C2-76B9-B5AB1024AA61}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discovery.Avalonia", "src\Discovery.Avalonia\Discovery.Avalonia.csproj", "{09326749-BBC2-21DE-BA52-6E3B4BB48BB6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discovery.Desktop", "src\Discovery.Desktop\Discovery.Desktop.csproj", "{E1BC3B5E-9798-0849-B8D1-EEDB326CDA8B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E3E8567F-52C2-4CE2-894D-13FD700D02AA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discovery.Loader.Avalonia", "test\Discovery.Loader.Avalonia\Discovery.Loader.Avalonia.csproj", "{C2CE7D56-2AEB-33AB-B0EA-6854925C8A22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discovery.Loader.Desktop", "test\Discovery.Loader\Discovery.Loader.Desktop.csproj", "{D7CBCD4D-F381-763E-9D57-6200BEC92F8C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {69E5A763-1B0B-A8C2-76B9-B5AB1024AA61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69E5A763-1B0B-A8C2-76B9-B5AB1024AA61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69E5A763-1B0B-A8C2-76B9-B5AB1024AA61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69E5A763-1B0B-A8C2-76B9-B5AB1024AA61}.Release|Any CPU.Build.0 = Release|Any CPU + {09326749-BBC2-21DE-BA52-6E3B4BB48BB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09326749-BBC2-21DE-BA52-6E3B4BB48BB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09326749-BBC2-21DE-BA52-6E3B4BB48BB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09326749-BBC2-21DE-BA52-6E3B4BB48BB6}.Release|Any CPU.Build.0 = Release|Any CPU + {E1BC3B5E-9798-0849-B8D1-EEDB326CDA8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1BC3B5E-9798-0849-B8D1-EEDB326CDA8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1BC3B5E-9798-0849-B8D1-EEDB326CDA8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1BC3B5E-9798-0849-B8D1-EEDB326CDA8B}.Release|Any CPU.Build.0 = Release|Any CPU + {C2CE7D56-2AEB-33AB-B0EA-6854925C8A22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2CE7D56-2AEB-33AB-B0EA-6854925C8A22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2CE7D56-2AEB-33AB-B0EA-6854925C8A22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2CE7D56-2AEB-33AB-B0EA-6854925C8A22}.Release|Any CPU.Build.0 = Release|Any CPU + {D7CBCD4D-F381-763E-9D57-6200BEC92F8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7CBCD4D-F381-763E-9D57-6200BEC92F8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7CBCD4D-F381-763E-9D57-6200BEC92F8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7CBCD4D-F381-763E-9D57-6200BEC92F8C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {69E5A763-1B0B-A8C2-76B9-B5AB1024AA61} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {09326749-BBC2-21DE-BA52-6E3B4BB48BB6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {E1BC3B5E-9798-0849-B8D1-EEDB326CDA8B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C2CE7D56-2AEB-33AB-B0EA-6854925C8A22} = {E3E8567F-52C2-4CE2-894D-13FD700D02AA} + {D7CBCD4D-F381-763E-9D57-6200BEC92F8C} = {E3E8567F-52C2-4CE2-894D-13FD700D02AA} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C88FC77E-459A-4DFD-9202-FB1070834AAB} + EndGlobalSection +EndGlobal diff --git a/dotnetcore.csproj b/dotnetcore.csproj deleted file mode 100644 index 120e38c..0000000 --- a/dotnetcore.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - Exe - net7.0 - - - diff --git a/src/Discovery.Avalonia/App.axaml b/src/Discovery.Avalonia/App.axaml new file mode 100644 index 0000000..2e65aa8 --- /dev/null +++ b/src/Discovery.Avalonia/App.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/Discovery.Avalonia/App.axaml.cs b/src/Discovery.Avalonia/App.axaml.cs new file mode 100644 index 0000000..6de3fc0 --- /dev/null +++ b/src/Discovery.Avalonia/App.axaml.cs @@ -0,0 +1,30 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Discovery.Avalonia.ViewModels; +using Discovery.Avalonia.Windows; + +namespace Discovery.Avalonia; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + //BindingPlugins.DataValidators.RemoveAt(0); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainViewModel() + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/src/Discovery.Avalonia/Assets/icon.ico b/src/Discovery.Avalonia/Assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6492fda9058dcbca8b2026c2f3444a6f11a75fec GIT binary patch literal 23462 zcmeHPX^a#_810!oVUHPh*_}Puo!Q8t5z&~CXpG^935pP0BUji62DzgNk{}8w8xuqg z;ZndO3Iq`35YJzTEQm%qMHH`?tVU3bf_SkiP`=mGGj{j%O!xH6?94R%k+=O-bya=! zy{@UMuC79oJoq0dC4eYtHNbev_hTjLia}w>5$+$C0ha-n0ha-n z0ha-nf$T8ghuJ%TM}SShF5m!gs2e~#v`4$NKN9c(xg@O5G+-Za0T4L^)@Ls;1*inF zCFJNa;4F|!{KTuv8Pu~K=x)x&!{!e_yn5ww{7KXsZ!j5`a#+|1B;>+d;S(j--6+aO zw26u_(}ZvG+>C>E%14YBCD+~{Xy2@kxW5i412Q3XDD)^`SdYTGCgGnpUo>nzC|ZtR z5G^M}_5k}0TMr2T)Q3f3ppRjF6+bxU1b}o(Jqr68h_TDM)GS&nLZAGTJ#G>_7uKuy zqL)NLpfRTIia%fB%65UT~!6tmdgrg7w$?gz%OWiJGb}(^FC^>elYgOP(UU)xEe&&=0+D z2v4%f7nck+zD-E*N8_I){fuyLAX!<344Sfh_de(do+5nt z?wPIePw{s}l03Xi3J5D;9tES!G8ga$>l9@b@1kt?g`y#(>Ctr)2`$m0M{D0PrDBS1T zSTtYRZDZ2LRp|J?md4W1;Xl%5)9Ujr8+upNXT|?L7P#WOWHcY&)vek~lrHe}PT}=e z%kLNd=?e_YY~FubUdy3d|eJ)kxx%i zvEVzDo%R&TbGdLuzPvrlMbkT-w)x6;uHyAqJ#HRmxM!ha)a1*St3>aE&HH~6T-#Rn zY2M0Sw)^^gZ&!)9Vn4$^O3f8@u5m{Cpk%I2=c<0=dof(~dHV~OD}%i-+;gRAR|N&O z_oC!`yDAwtSkx}t8jB^H<$Z7(p78rC9X!NVo8oUZ-?`FrVHOUodc&DqSy&e}<3@eI zymg{&o>FL6IkVob<|2_^F?xy!9s4^au57NiYxX*3>pM;&Zg`LTXZf9~etkzu97X08 zHoxO=Pk(fLDH}F6Ep=g?tbQL19ylk0AD)SeOng5q8#*fLv*Q0AT+QSKB#7qYdwhg@ zCttZ|O6@oF=gf%0{TaGEqIvHrNAuP6?ss~g80NVdGr0;0GsL)4RQ?2gzyad79id;lHu`#iVJ^V^x02{3RPuu`X7OT)FywoDVGE58@Qi-B`MuJK`1AI=pQ0UWEc zCA2*g_yD*F7}Yadajeh#z;qy8;~a`gi&UeOJArm!Gq4Ld2=Huwra?Qj_cE{$Xa%YO zS6l{M23!VQ23!VQ23!WZl>rfvPD~;rlCq8hSe#rARVgUz{2$>YsYKShWW7MvJ7ryx z^^UN<5OJbCtPi0cR;8d$zof+b5Faic26TFt6qW+S$J05fr+7NUi@~m*c^=|D3w3i+>7ZCSGb literal 0 HcmV?d00001 diff --git a/src/Discovery.Avalonia/Converters/MoveConverters.cs b/src/Discovery.Avalonia/Converters/MoveConverters.cs new file mode 100644 index 0000000..95aeda2 --- /dev/null +++ b/src/Discovery.Avalonia/Converters/MoveConverters.cs @@ -0,0 +1,47 @@ +using Avalonia.Data.Converters; +using Discovery.Avalonia.ViewModels; +using System.Globalization; + +namespace Discovery.Avalonia.Converters; + +public class CanMoveUpConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return true; + + if (parameter is IEnumerable items && value is object item) + { + var list = items.Cast().ToList(); + var idx = list.IndexOf(item); + return idx > 0; + } + return false; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +public class CanMoveDownConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return true; + + if (parameter is IEnumerable items && value is object item) + { + var list = items.Cast().ToList(); + var idx = list.IndexOf(item); + return idx >= 0 && idx < list.Count - 1; + } + return false; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Discovery.Avalonia/Discovery.Avalonia.csproj b/src/Discovery.Avalonia/Discovery.Avalonia.csproj new file mode 100644 index 0000000..6637fde --- /dev/null +++ b/src/Discovery.Avalonia/Discovery.Avalonia.csproj @@ -0,0 +1,53 @@ + + + net9.0 + latest + enable + enable + Discovery.Avalonia + Discovery + Kevin Kai Berthold + Webmatic GmbH + 2025.3.14.0 + 2025.3.14.0 + 2025.3.14.0 + true + true + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MainView.axaml + + + diff --git a/src/Discovery.Avalonia/Models/TargetViewModel.cs b/src/Discovery.Avalonia/Models/TargetViewModel.cs new file mode 100644 index 0000000..5d48b30 --- /dev/null +++ b/src/Discovery.Avalonia/Models/TargetViewModel.cs @@ -0,0 +1,42 @@ +using Discovery.Avalonia.Windows; +using Discovery.Models; +using System.Diagnostics; +using System.Net; + +namespace Discovery.Avalonia.Models; + +public class TargetViewModel(int index, IPAddress ipAddress) : Target(index, ipAddress) +{ + public void CopyAddress() => SetClipboard(Address); + public void CopyHostname() => SetClipboard(Hostname); + public void CopyMac() => SetClipboard(Mac); + public void CopyVendor() => SetClipboard(Vendor); + + public void HttpAddress() => OpenHref(Address, false); + public void HttpsAddress() => OpenHref(Address, true); + public void HttpHostname() => OpenHref(Hostname, false); + public void HttpsHostname() => OpenHref(Hostname, true); + + private static void OpenHref(string? uri, bool secure) + { + if (uri == null) return; + + uri = secure ? "https://" + uri : "http://" + uri; + + try + { + Process.Start(new ProcessStartInfo { FileName = uri, UseShellExecute = true }); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } + + public static void SetClipboard(string? text) + { + if (text == null) return; + if (MainWindow.Instance?.Clipboard == null) return; + _ = MainWindow.Instance.Clipboard.SetTextAsync(text); + } +} \ No newline at end of file diff --git a/src/Discovery.Avalonia/ViewModels/MainViewModel.cs b/src/Discovery.Avalonia/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..b278071 --- /dev/null +++ b/src/Discovery.Avalonia/ViewModels/MainViewModel.cs @@ -0,0 +1,354 @@ +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Discovery.Avalonia.Models; +using Discovery.Avalonia.Windows; +using Discovery.Helpers; +using Discovery.Models; +using Discovery.Services; +using System.Net; + +namespace Discovery.Avalonia.ViewModels; + +public partial class MainViewModel : ViewModelBase +{ + public string? HostInput + { + get + { + return _hostInput; + } + set + { + if (value != _hostInput) + { + _hostInput = value; + OnPropertyChanged(nameof(HostInput)); + } + } + } + + public int OnlineFilter + { + get { return _onlineInput; } + set + { + if (value != _onlineInput) + { + _onlineInput = value; + OnFilterChanged(); + OnPropertyChanged(nameof(OnlineFilter)); + } + } + } + + public string? RegexFilter + { + get + { + return _regexInput; + } + set + { + if (value != _regexInput) + { + _regexInput = value; + OnFilterChanged(); + OnPropertyChanged(nameof(RegexFilter)); + } + } + } + + public string? ScanText + { + get + { + return _scanText; + } + set + { + if (value != _scanText) + { + _scanText = value; + OnPropertyChanged(nameof(ScanText)); + } + } + } + + public string? StatusMessage + { + get + { + return _status; + } + set + { + if (value != _status) + { + _status = value; + OnPropertyChanged(nameof(StatusMessage)); + } + } + } + + public string? StatusMessage2 + { + get + { + return _status2; + } + set + { + if (value != _status2) + { + _status2 = value; + OnPropertyChanged(nameof(StatusMessage2)); + } + } + } + + public double Progress + { + get + { + return _progress; + } + set + { + if (value != _progress) + { + _progress = value; + OnPropertyChanged(nameof(Progress)); + } + } + } + + public string? Version + { + get + { + return _version; + } + set + { + if (value != _version) + { + _version = value; + OnPropertyChanged(nameof(Version)); + } + } + } + + public ScannerOptions Options { get; set; } = new(); + public SynchronizedCollection Targets { get; } + public RelayCommand ScanCommand { get; } + public RelayCommand ExitCommand { get; } + public RelayCommand ExportCsvCommand { get; } + public RelayCommand ExportHtmlCommand { get; } + public RelayCommand TestCommand { get; } + + private string? _version = "2025.8.19.0"; + private string? _hostInput; + private string? _regexInput; + private int _onlineInput = 1; + private string? _scanText = "Scan"; + private string? _status; + private string? _status2; + private double _progress; + + private readonly SynchronizationContext _uiContext; + private readonly NetworkInfo? _networkInfo; + private readonly Scanner _scanner = new(); + + public MainViewModel() + { + _uiContext = SynchronizationContext.Current ?? throw new Exception("SynchronizationContext"); + _networkInfo = _scanner.GetPrimaryNetwork(); + + if (_networkInfo != null) + { + HostInput = $"{_networkInfo.Network}/{_networkInfo.Cidr}"; + Options.Nameservers = _networkInfo.Nameservers; + } + + Targets = new(p => p.Index, _uiContext); + + ScanCommand = new(async () => { await OnScanAsync(); }); + ExitCommand = new(Shutdown); + ExportCsvCommand = new(async () => { await ExportCsvAsync(); }); + ExportHtmlCommand = new(async () => { await ExportHtmlAsync(); }); + TestCommand = new(async () => { await OpenOptionDialog(); }); + + // init filter, needed + OnFilterChanged(); + } + + public async Task OnScanAsync() + { + if (_scanner.State.Operation == ScanOperation.Scanning) + { + await _scanner.CancelAsync(); + return; + } + + if (string.IsNullOrWhiteSpace(_hostInput) || + !IpHelper.TryParseHostNotation(_hostInput, out var address, out var cidr)) + { + StatusMessage = "Invalid Address"; + return; + } + + if (IPNetwork2.TryParse($"{address}/{cidr}", out var network) is false) + { + StatusMessage = "Invalid Network"; + return; + } + + var addresses = network.ListIPAddress(filter: Filter.Usable); + + Targets.Clear(); + + for (int i = 0; i < addresses.Count; i++) + { + Targets.Add(new TargetViewModel(i, addresses[i])); + } + + var events = new ScannerEvents() + { + OnStateUpdate = (state) => Task.Run(() => + { + Dispatcher.UIThread.Invoke(() => + { + Progress = state.Progress; + }, DispatcherPriority.Default, default); + }, default) + }; + + var scanTask = _scanner.ScanAsync(Targets, Options, events, default).ConfigureAwait(false); + + ScanText = "Abort"; + StatusMessage = $"Scanning..."; + StatusMessage2 = ""; + + var result = await scanTask; + if (result.Operation == ScanOperation.Idle) + { + ScanText = "Scan"; + StatusMessage = $"Scanned {addresses.Count} Hosts in: {result.Elapsed}"; + StatusMessage2 = $"{Targets.Where(p => p.Online).Count()} Online"; + } + else + { + ScanText = "Scan"; + StatusMessage = "Scan aborted!"; + StatusMessage2 = $"{Targets.Where(p => p.Online).Count()} Online"; + } + } + + public static void Shutdown() + { + Dispatcher.UIThread.InvokeShutdown(); + } + + private void OnFilterChanged() + { + bool online(Target p) => _onlineInput switch + { + 0 => true, + 1 => p.Online == true, + 2 => p.Online == false, + _ => true, + }; + + bool regex(Target p) + { + if (string.IsNullOrWhiteSpace(_regexInput)) return true; + if (p.Address != null && p.Address.Contains(_regexInput, StringComparison.InvariantCultureIgnoreCase)) return true; + if (p.Hostname != null && p.Hostname.ToString().Contains(_regexInput, StringComparison.InvariantCultureIgnoreCase)) return true; + if (p.Mac != null && p.Mac.ToString().Contains(_regexInput, StringComparison.InvariantCultureIgnoreCase)) return true; + if (p.Vendor != null && p.Vendor.ToString().Contains(_regexInput, StringComparison.InvariantCultureIgnoreCase)) return true; + return false; + } + + Targets.Filter = (x) => online(x) && regex(x); + + // force re-render (avalonia render bug) + if (MainWindow.Instance != null) + { + Dispatcher.UIThread.Invoke(async () => + { + await Task.Delay(50); + MainWindow.Instance.Width = MainWindow.Instance.Width + 2; + }, DispatcherPriority.Send); + + Dispatcher.UIThread.Invoke(async () => + { + await Task.Delay(50); + MainWindow.Instance.Width = MainWindow.Instance.Width - 2; + }, DispatcherPriority.Send); + } + } + + private async Task ExportCsvAsync() + { + if (Targets is null) return; + + var pre = $"discovery_{DateTime.Now:MM-dd-yyyy_hh-mm-ss}"; + + var topLevel = TopLevel.GetTopLevel(MainWindow.Instance); + if (topLevel is null) return; + + var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + { + SuggestedFileName = pre, + DefaultExtension = "csv", + ShowOverwritePrompt = true, + Title = "Save as CSV" + }); + + if (file is null || file.Path is null) + return; + + await Exporter.ExportAsCsvAsync(Targets, file.Path.LocalPath); + + await Dispatcher.UIThread.InvokeAsync(() => + { + StatusMessage = $"Exported as CSV to: {file.Path.LocalPath}"; + }, DispatcherPriority.Default, default); + } + + private async Task ExportHtmlAsync() + { + if (Targets is null) return; + + var pre = $"discovery_{DateTime.Now:MM-dd-yyyy_hh-mm-ss}"; + + var topLevel = TopLevel.GetTopLevel(MainWindow.Instance); + if (topLevel is null) return; + + var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + { + SuggestedFileName = pre, + DefaultExtension = "html", + ShowOverwritePrompt = true, + Title = "Save as HTML" + }); + + if (file is null || file.Path is null) + return; + + await Exporter.ExportAsHtmlAsync(Targets, file.Path.LocalPath); + + await Dispatcher.UIThread.InvokeAsync(() => + { + StatusMessage = $"Exported as HTML to: {file.Path.LocalPath}"; + }, DispatcherPriority.Default, default); + } + + private async Task OpenOptionDialog() + { + var option = await WeakReferenceMessenger.Default.Send(new OpenOptionsMessage()); + } +} \ No newline at end of file diff --git a/src/Discovery.Avalonia/ViewModels/OptionViewModel.cs b/src/Discovery.Avalonia/ViewModels/OptionViewModel.cs new file mode 100644 index 0000000..4d0120d --- /dev/null +++ b/src/Discovery.Avalonia/ViewModels/OptionViewModel.cs @@ -0,0 +1,92 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging.Messages; +using System.Collections.ObjectModel; +using System.Windows.Input; + +namespace Discovery.Avalonia.ViewModels; + +public class OpenOptionsMessage : AsyncRequestMessage; + +public class OptionViewModel : ViewModelBase +{ + public ObservableCollection Nameservers { get; } = []; + public int? SelectedNameserver { get; set; } + public ICommand AddCommand { get; } + public ICommand RemoveCommand { get; } + public ICommand MoveUpCommand { get; } + public ICommand MoveDownCommand { get; } + + public OptionViewModel() + { + AddCommand = new RelayCommand(Add); + RemoveCommand = new RelayCommand(Remove); + MoveUpCommand = new RelayCommand(MoveUp); + MoveDownCommand = new RelayCommand(MoveDown); + + //if (Design.IsDesignMode) + //{ + // Nameservers.Add(new Nameserver { Host = "1.1.1.1"} ); + // Nameservers.Add(new Nameserver { Host = "8.8.8.8" }); + //} + } + + private void Add() + { + Nameservers.Add(new Nameserver { Host = "127.0.0.1" }); + OnPropertyChanged(nameof(Nameservers)); + } + + private void Remove() + { + if (SelectedNameserver == null) + return; + + Nameservers.RemoveAt(SelectedNameserver.Value); + OnPropertyChanged(nameof(Nameservers)); + } + + private void MoveUp(Nameserver? item) + { + Console.WriteLine("MoveUp"); + + if (item == null) + { + Console.WriteLine("no item"); + return; + } + int index = Nameservers.IndexOf(item); + + if (index > 0) + { + Nameservers.RemoveAt(index); + Nameservers.Insert(index - 1, item); + } + + OnPropertyChanged(nameof(Nameservers)); + } + + private void MoveDown(Nameserver? item) + { + Console.WriteLine("MoveDown"); + + if (item == null) + { + Console.WriteLine("no item"); + return; + } + int index = Nameservers.IndexOf(item); + + if (index >= 0 && index < Nameservers.Count - 1) + { + Nameservers.RemoveAt(index); + Nameservers.Insert(index + 1, item); + } + + OnPropertyChanged(nameof(Nameservers)); + } +} + +public class Nameserver +{ + public string Host { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Discovery.Avalonia/ViewModels/ViewModelBase.cs b/src/Discovery.Avalonia/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..019d25a --- /dev/null +++ b/src/Discovery.Avalonia/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Discovery.Avalonia.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} diff --git a/src/Discovery.Avalonia/Views/MainView.axaml b/src/Discovery.Avalonia/Views/MainView.axaml new file mode 100644 index 0000000..026f686 --- /dev/null +++ b/src/Discovery.Avalonia/Views/MainView.axaml @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Alle + Online + Offline + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Discovery.Avalonia/Views/MainView.axaml.cs b/src/Discovery.Avalonia/Views/MainView.axaml.cs new file mode 100644 index 0000000..605fc24 --- /dev/null +++ b/src/Discovery.Avalonia/Views/MainView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Discovery.Avalonia.Views; + +public partial class MainView : UserControl +{ + public MainView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Discovery.Avalonia/Windows/MainWindow.axaml b/src/Discovery.Avalonia/Windows/MainWindow.axaml new file mode 100644 index 0000000..4ad7e82 --- /dev/null +++ b/src/Discovery.Avalonia/Windows/MainWindow.axaml @@ -0,0 +1,15 @@ + + + diff --git a/src/Discovery.Avalonia/Windows/MainWindow.axaml.cs b/src/Discovery.Avalonia/Windows/MainWindow.axaml.cs new file mode 100644 index 0000000..d56269c --- /dev/null +++ b/src/Discovery.Avalonia/Windows/MainWindow.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.Messaging; +using Discovery.Avalonia.ViewModels; + +namespace Discovery.Avalonia.Windows; + +public partial class MainWindow : Window +{ + public static MainWindow? Instance { get; set; } + + public MainWindow() + { + Instance = this; + InitializeComponent(); + + if (Design.IsDesignMode) + return; + + // Whenever 'Send(new PurchaseAlbumMessage())' is called, invoke this callback on the MainWindow instance: + WeakReferenceMessenger.Default.Register(this, static (w, m) => + { + // Create an instance of MusicStoreWindow and set MusicStoreViewModel as its DataContext. + var dialog = new OptionWindow + { + DataContext = new OptionViewModel() + }; + + // Show dialog window and reply with returned AlbumViewModel or null when the dialog is closed. + m.Reply(dialog.ShowDialog(w)); + }); + } +} \ No newline at end of file diff --git a/src/Discovery.Avalonia/Windows/OptionWindow.axaml b/src/Discovery.Avalonia/Windows/OptionWindow.axaml new file mode 100644 index 0000000..0d0ec79 --- /dev/null +++ b/src/Discovery.Avalonia/Windows/OptionWindow.axaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Concurrency + + + + + + + + + + + + + + + Timeout (ms) + + + + + + + + + + + + + + + + + + + + + Timeout (ms) + + + + TTL + + + + Dont Fragment + + + + + + + + + + + + + + + +