migration
This commit is contained in:
parent
561881c9aa
commit
f133c740e1
55 changed files with 2928 additions and 20 deletions
12
Program.cs
12
Program.cs
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace dotnetcore
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello World!");
|
||||
}
|
||||
}
|
||||
}
|
||||
60
discovery.sln
Normal file
60
discovery.sln
Normal file
|
|
@ -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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
16
src/Discovery.Avalonia/App.axaml
Normal file
16
src/Discovery.Avalonia/App.axaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Discovery.Avalonia.App"
|
||||
RequestedThemeVariant="Light">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme>
|
||||
<FluentTheme.Palettes>
|
||||
<ColorPaletteResources x:Key="Light" Accent="#2f0079d9" AltHigh="White" AltLow="White" AltMedium="White" AltMediumHigh="White" AltMediumLow="White" BaseHigh="Black" BaseLow="#ffcccccc" BaseMedium="#ff898989" BaseMediumHigh="#ff5d5d5d" BaseMediumLow="#ff737373" ChromeAltLow="#ff5d5d5d" ChromeBlackHigh="Black" ChromeBlackLow="#ffcccccc" ChromeBlackMedium="#ff5d5d5d" ChromeBlackMediumLow="#ff898989" ChromeDisabledHigh="#ffcccccc" ChromeDisabledLow="#ff898989" ChromeGray="#ff737373" ChromeHigh="#ffcccccc" ChromeLow="#ffececec" ChromeMedium="#ffe6e6e6" ChromeMediumLow="#ffececec" ChromeWhite="White" ListLow="#ffe6e6e6" ListMedium="#ffcccccc" RegionColor="White" />
|
||||
<ColorPaletteResources x:Key="Dark" Accent="#ff0073cf" AltHigh="Black" AltLow="Black" AltMedium="Black" AltMediumHigh="Black" AltMediumLow="Black" BaseHigh="White" BaseLow="#ff333333" BaseMedium="#ff9a9a9a" BaseMediumHigh="#ffb4b4b4" BaseMediumLow="#ff676767" ChromeAltLow="#ffb4b4b4" ChromeBlackHigh="Black" ChromeBlackLow="#ffb4b4b4" ChromeBlackMedium="Black" ChromeBlackMediumLow="Black" ChromeDisabledHigh="#ff333333" ChromeDisabledLow="#ff9a9a9a" ChromeGray="Gray" ChromeHigh="Gray" ChromeLow="#ff151515" ChromeMedium="#ff1d1d1d" ChromeMediumLow="#ff2c2c2c" ChromeWhite="White" ListLow="#ff1d1d1d" ListMedium="#ff333333" RegionColor="Black" />
|
||||
</FluentTheme.Palettes>
|
||||
</FluentTheme>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
30
src/Discovery.Avalonia/App.axaml.cs
Normal file
30
src/Discovery.Avalonia/App.axaml.cs
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
BIN
src/Discovery.Avalonia/Assets/icon.ico
Normal file
BIN
src/Discovery.Avalonia/Assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
47
src/Discovery.Avalonia/Converters/MoveConverters.cs
Normal file
47
src/Discovery.Avalonia/Converters/MoveConverters.cs
Normal file
|
|
@ -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<Nameserver> items && value is object item)
|
||||
{
|
||||
var list = items.Cast<object>().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<Nameserver> items && value is object item)
|
||||
{
|
||||
var list = items.Cast<object>().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();
|
||||
}
|
||||
}
|
||||
53
src/Discovery.Avalonia/Discovery.Avalonia.csproj
Normal file
53
src/Discovery.Avalonia/Discovery.Avalonia.csproj
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Discovery.Avalonia</RootNamespace>
|
||||
<Product>Discovery</Product>
|
||||
<Authors>Kevin Kai Berthold</Authors>
|
||||
<Copyright>Webmatic GmbH</Copyright>
|
||||
<AssemblyVersion>2025.3.14.0</AssemblyVersion>
|
||||
<PackageVersion>2025.3.14.0</PackageVersion>
|
||||
<FileVersion>2025.3.14.0</FileVersion>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
|
||||
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
|
||||
<EnableAotAnalyzer>true</EnableAotAnalyzer>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--Trimmer Requirenments-->
|
||||
<ItemGroup>
|
||||
<TrimmerRootAssembly Include="Avalonia.Controls.DataGrid" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\icon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Discovery.Core\Discovery.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Views\MainView.axaml.cs">
|
||||
<DependentUpon>MainView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
42
src/Discovery.Avalonia/Models/TargetViewModel.cs
Normal file
42
src/Discovery.Avalonia/Models/TargetViewModel.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
354
src/Discovery.Avalonia/ViewModels/MainViewModel.cs
Normal file
354
src/Discovery.Avalonia/ViewModels/MainViewModel.cs
Normal file
|
|
@ -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<TargetViewModel> 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<TargetViewModel> _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());
|
||||
}
|
||||
}
|
||||
92
src/Discovery.Avalonia/ViewModels/OptionViewModel.cs
Normal file
92
src/Discovery.Avalonia/ViewModels/OptionViewModel.cs
Normal file
|
|
@ -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<OptionViewModel?>;
|
||||
|
||||
public class OptionViewModel : ViewModelBase
|
||||
{
|
||||
public ObservableCollection<Nameserver> 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<Nameserver>(MoveUp);
|
||||
MoveDownCommand = new RelayCommand<Nameserver>(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!;
|
||||
}
|
||||
7
src/Discovery.Avalonia/ViewModels/ViewModelBase.cs
Normal file
7
src/Discovery.Avalonia/ViewModels/ViewModelBase.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Discovery.Avalonia.ViewModels;
|
||||
|
||||
public class ViewModelBase : ObservableObject
|
||||
{
|
||||
}
|
||||
280
src/Discovery.Avalonia/Views/MainView.axaml
Normal file
280
src/Discovery.Avalonia/Views/MainView.axaml
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Discovery.Avalonia.Views.MainView"
|
||||
xmlns:vm="clr-namespace:Discovery.Avalonia.ViewModels"
|
||||
xmlns:models="using:Discovery.Avalonia.Models"
|
||||
x:DataType="vm:MainViewModel">
|
||||
<Design.DataContext>
|
||||
<vm:MainViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid>
|
||||
<Grid.Resources>
|
||||
<SolidColorBrush x:Key="GrayBackground" Color="White" Opacity=".8" />
|
||||
<SolidColorBrush x:Key="GrayBackground2" Color="WhiteSmoke" Opacity=".3" />
|
||||
<SolidColorBrush x:Key="GrayBackground3" Color="WhiteSmoke" Opacity="1" />
|
||||
<SolidColorBrush x:Key="GrayBorder" Color="LightGray" Opacity="1" />
|
||||
<SolidColorBrush x:Key="BlackBorder" Color="Black" Opacity=".3" />
|
||||
<SolidColorBrush x:Key="PrimarySelector" Color="Red" Opacity=".3" />
|
||||
<SolidColorBrush x:Key="SecondarySelector" Color="Orange" Opacity=".3" />
|
||||
</Grid.Resources>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<DockPanel Grid.Column="0">
|
||||
<Menu DockPanel.Dock="Top">
|
||||
<MenuItem Header="_Datei">
|
||||
<MenuItem Header="_Export">
|
||||
<MenuItem Header="_CSV"
|
||||
Command="{Binding ExportCsvCommand}" />
|
||||
<MenuItem Header="_HTML"
|
||||
Command="{Binding ExportHtmlCommand}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Beenden"
|
||||
Command="{Binding ExitCommand}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Options" Command="{Binding TestCommand}">
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</DockPanel>
|
||||
<TextBlock Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,-10,3,0"
|
||||
Opacity="0.3"
|
||||
FontSize="12"
|
||||
Text="{Binding Version, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<Grid Grid.Row="1"
|
||||
Height="40">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
MinWidth="200"
|
||||
Margin="5,5,15,5"
|
||||
Background="{StaticResource GrayBackground}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding HostInput}"
|
||||
Watermark="IP/CIDR">
|
||||
</TextBox>
|
||||
<TextBox Grid.Column="1"
|
||||
MinWidth="200"
|
||||
Margin="5,5,15,5"
|
||||
Background="{StaticResource GrayBackground}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding RegexFilter}"
|
||||
Watermark="Filter">
|
||||
</TextBox>
|
||||
<Grid Grid.Column="3"
|
||||
Margin="0,0,5,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<ComboBox Margin="0,0,0,0"
|
||||
BorderThickness="0"
|
||||
SelectedIndex="{Binding OnlineFilter}">
|
||||
<ComboBoxItem>Alle</ComboBoxItem>
|
||||
<ComboBoxItem>Online</ComboBoxItem>
|
||||
<ComboBoxItem>Offline</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
<Button Grid.Column="4"
|
||||
Width="78"
|
||||
Margin="0,5,5,5"
|
||||
Background="{StaticResource GrayBackground3}"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding OnScanAsync}">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{Binding ScanText}"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<DataGrid Grid.Row="2"
|
||||
Background="{StaticResource GrayBackground2}"
|
||||
BorderBrush="{StaticResource BlackBorder}"
|
||||
BorderThickness="0,0,0,0"
|
||||
GridLinesVisibility="All"
|
||||
ItemsSource="{Binding Targets, Mode=TwoWay}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="False"
|
||||
CanUserReorderColumns="False">
|
||||
<DataGrid.Styles>
|
||||
<Style Selector="DataGridColumnHeader:nth-child(1),
|
||||
DataGridColumnHeader:nth-child(2),
|
||||
DataGridColumnHeader:nth-child(3),
|
||||
DataGridColumnHeader:nth-child(4),
|
||||
DataGridColumnHeader:nth-child(5)">
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0,0,0,0" />
|
||||
<Setter Property="Padding" Value="8,0,0,0" />
|
||||
<Setter Property="Height" Value="20" />
|
||||
<Setter Property="TextBlock.FontSize" Value="13" />
|
||||
<Setter Property="TextBlock.FontWeight" Value="DemiBold" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="BorderBrush" Value="Black" />
|
||||
<Setter Property="BorderThickness" Value=".4,.4,0,.4" />
|
||||
<!--<Setter Property="Foreground" Value="Red" />-->
|
||||
</Style>
|
||||
<Style Selector="DataGridCell:current /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible"
|
||||
Value="false" />
|
||||
</Style>
|
||||
<!--<Style Selector="DataGridRow:pointerover /template/ Rectangle">
|
||||
<Setter Property="Fill" Value="Transparent"/>
|
||||
</Style>-->
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn MinWidth="150"
|
||||
Width="Auto"
|
||||
Header="Address" >
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="Transparent">
|
||||
<TextBlock FontWeight="SemiLight"
|
||||
FontSize="14"
|
||||
Margin="7,7,7,7"
|
||||
Text="{Binding Address}" />
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Copy"
|
||||
Command="{Binding CopyAddress}" />
|
||||
<MenuItem Header="Open (Http)"
|
||||
Command="{Binding HttpAddress}" />
|
||||
<MenuItem Header="Open (Https)"
|
||||
Command="{Binding HttpsAddress}" />
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn MinWidth="180"
|
||||
Width="Auto"
|
||||
Header="Hostname" >
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="Transparent">
|
||||
<TextBlock Margin="7,7,7,7"
|
||||
FontWeight="SemiLight"
|
||||
FontSize="14"
|
||||
Text="{Binding Hostname}" />
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Copy"
|
||||
Command="{Binding CopyHostname}" />
|
||||
<MenuItem Header="Open (Http)"
|
||||
Command="{Binding HttpHostname}" />
|
||||
<MenuItem Header="Open (Https)"
|
||||
Command="{Binding HttpsHostname}" />
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn MinWidth="150"
|
||||
Width="Auto"
|
||||
Header="MAC" >
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="Transparent">
|
||||
<TextBlock Margin="7,7,7,7"
|
||||
FontWeight="SemiLight"
|
||||
FontSize="14"
|
||||
Text="{Binding Mac}" />
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Copy"
|
||||
Command="{Binding CopyMac}" />
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn MinWidth="200"
|
||||
Width="*"
|
||||
Header="Hersteller">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="Transparent">
|
||||
<TextBlock Margin="7,7,7,7"
|
||||
FontWeight="SemiLight"
|
||||
FontSize="14"
|
||||
Text="{Binding Vendor}" />
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Copy"
|
||||
Command="{Binding CopyVendor}" />
|
||||
</ContextMenu>
|
||||
</Border.ContextMenu>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn MinWidth="50"
|
||||
Width="Auto"
|
||||
Header="Online">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="Transparent"
|
||||
BorderThickness="0,0,0,0">
|
||||
<Viewbox Height="28"
|
||||
HorizontalAlignment="Center">
|
||||
<CheckBox IsChecked="{Binding Online}"
|
||||
IsEnabled="False" />
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
<!--<DataGrid.RowDetailsTemplate>
|
||||
<DataTemplate>
|
||||
</DataTemplate>
|
||||
</DataGrid.RowDetailsTemplate>-->
|
||||
</DataGrid>
|
||||
<Border Grid.Column="3"
|
||||
BorderBrush="{StaticResource GrayBorder}"
|
||||
BorderThickness="0,1,0,0" />
|
||||
|
||||
<Grid Grid.Row="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Margin="5,5,0,5"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding StatusMessage}" />
|
||||
<TextBlock Grid.Column="2"
|
||||
Margin="0,5,5,5"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{Binding StatusMessage2}" />
|
||||
</Grid>
|
||||
<ProgressBar Grid.Row="5"
|
||||
Height="5"
|
||||
Background="WhiteSmoke"
|
||||
Foreground="#64B5F6"
|
||||
Minimum="0" Maximum="100"
|
||||
Value="{Binding Progress}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
src/Discovery.Avalonia/Views/MainView.axaml.cs
Normal file
11
src/Discovery.Avalonia/Views/MainView.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace Discovery.Avalonia.Views;
|
||||
|
||||
public partial class MainView : UserControl
|
||||
{
|
||||
public MainView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
15
src/Discovery.Avalonia/Windows/MainWindow.axaml
Normal file
15
src/Discovery.Avalonia/Windows/MainWindow.axaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:Discovery.Avalonia.ViewModels"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:Discovery.Avalonia.Views"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Discovery.Avalonia.Windows.MainWindow"
|
||||
Icon="/Assets/icon.ico"
|
||||
Title="Webmatic Discovery"
|
||||
Width="900"
|
||||
Height="550"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
||||
<views:MainView />
|
||||
</Window>
|
||||
32
src/Discovery.Avalonia/Windows/MainWindow.axaml.cs
Normal file
32
src/Discovery.Avalonia/Windows/MainWindow.axaml.cs
Normal file
|
|
@ -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<MainWindow, OpenOptionsMessage>(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<OptionViewModel?>(w));
|
||||
});
|
||||
}
|
||||
}
|
||||
177
src/Discovery.Avalonia/Windows/OptionWindow.axaml
Normal file
177
src/Discovery.Avalonia/Windows/OptionWindow.axaml
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="400"
|
||||
x:Class="Discovery.Avalonia.Windows.OptionWindow"
|
||||
xmlns:vm="clr-namespace:Discovery.Avalonia.ViewModels"
|
||||
xmlns:models="using:Discovery.Avalonia.Models"
|
||||
xmlns:conv="using:Discovery.Avalonia.Converters"
|
||||
x:DataType="vm:OptionViewModel"
|
||||
Title="OptionWindow"
|
||||
Width="500" Height="400"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Design.DataContext>
|
||||
<vm:OptionViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Window.Resources>
|
||||
<conv:CanMoveUpConverter x:Key="CanMoveUpConverter" />
|
||||
<conv:CanMoveDownConverter x:Key="CanMoveDownConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TabControl Margin="10">
|
||||
<TabItem Header="General">
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="20" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
Concurrency
|
||||
</TextBlock>
|
||||
<TextBox Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text=""
|
||||
Watermark="128" />
|
||||
|
||||
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="ARP">
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
Timeout (ms)
|
||||
</TextBlock>
|
||||
<TextBox Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text=""
|
||||
Watermark="100">
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="ICMP">
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
Timeout (ms)
|
||||
</TextBlock>
|
||||
<TextBox Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text=""
|
||||
Watermark="100" />
|
||||
<TextBlock Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
TTL
|
||||
</TextBlock>
|
||||
<TextBox Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text=""
|
||||
Watermark="128" />
|
||||
<TextBlock Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
Dont Fragment
|
||||
</TextBlock>
|
||||
<ToggleSwitch Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="DNS">
|
||||
<StackPanel Margin="0,10,0,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Button Grid.Column="0" Margin="0,0,0,10" Content="Add" Command="{Binding AddCommand}"/>
|
||||
<Button Grid.Column="2" Margin="0,0,0,10" Content="Remove" Command="{Binding RemoveCommand}" />
|
||||
</Grid>
|
||||
|
||||
<DataGrid ItemsSource="{Binding Nameservers, Mode=TwoWay}"
|
||||
AutoGenerateColumns="False"
|
||||
HeadersVisibility="None"
|
||||
SelectedIndex="{Binding SelectedNameserver}"
|
||||
SelectionMode="Single">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding Host}" Width="*" />
|
||||
<DataGridTemplateColumn Header="Move">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Content="▲"
|
||||
Background="Transparent"
|
||||
Command="{Binding $parent[DataGrid].DataContext.MoveUpCommand}"
|
||||
CommandParameter="{Binding .}"
|
||||
IsEnabled="{Binding $parent[DataGridRow].DataContext,
|
||||
Converter={StaticResource CanMoveUpConverter},
|
||||
ConverterParameter={Binding $parent[DataGrid].ItemsSource}}"/>
|
||||
<Button Content="▼"
|
||||
Background="Transparent"
|
||||
Command="{Binding $parent[DataGrid].DataContext.MoveDownCommand}"
|
||||
CommandParameter="{Binding .}"
|
||||
IsEnabled="{Binding $parent[DataGridRow].DataContext,
|
||||
Converter={StaticResource CanMoveDownConverter},
|
||||
ConverterParameter={Binding $parent[DataGrid].ItemsSource}}"
|
||||
/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Window>
|
||||
11
src/Discovery.Avalonia/Windows/OptionWindow.axaml.cs
Normal file
11
src/Discovery.Avalonia/Windows/OptionWindow.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace Discovery.Avalonia.Windows;
|
||||
|
||||
public partial class OptionWindow : Window
|
||||
{
|
||||
public OptionWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
35
src/Discovery.Core/Discovery.Core.csproj
Normal file
35
src/Discovery.Core/Discovery.Core.csproj
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Discovery.Core</AssemblyName>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Discovery</RootNamespace>
|
||||
<Product>Discovery</Product>
|
||||
<Authors>Kevin Kai Berthold</Authors>
|
||||
<Copyright>Webmatic GmbH</Copyright>
|
||||
<AssemblyVersion>2025.3.14.0</AssemblyVersion>
|
||||
<PackageVersion>2025.3.14.0</PackageVersion>
|
||||
<FileVersion>2025.3.14.0</FileVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
|
||||
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
|
||||
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
|
||||
<EnableAotAnalyzer>true</EnableAotAnalyzer>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IPNetwork2" Version="3.4.832" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="vendors.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
24
src/Discovery.Core/Helpers/IpHelper.cs
Normal file
24
src/Discovery.Core/Helpers/IpHelper.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Discovery.Helpers;
|
||||
|
||||
public static class IpHelper
|
||||
{
|
||||
public static bool TryParseHostNotation(string input, out IPAddress? host, out int cidr)
|
||||
{
|
||||
cidr = 32;
|
||||
|
||||
var cidrNotation = input.Contains('/');
|
||||
|
||||
var hostRaw = cidrNotation ? input.Split("/")[0] : input;
|
||||
var cidrRaw = cidrNotation ? input.Split("/")[1] : "32";
|
||||
|
||||
if (IPAddress.TryParse(hostRaw, out host) is false)
|
||||
return false;
|
||||
|
||||
if (int.TryParse(cidrRaw, out cidr) is false)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
5
src/Discovery.Core/Models/NetworkInfo.cs
Normal file
5
src/Discovery.Core/Models/NetworkInfo.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Discovery.Models;
|
||||
|
||||
public record NetworkInfo(IPAddress Client, IPAddress Network, int Cidr, List<IPAddress>? Nameservers);
|
||||
6
src/Discovery.Core/Models/ScannerEvents.cs
Normal file
6
src/Discovery.Core/Models/ScannerEvents.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace Discovery.Models;
|
||||
|
||||
public class ScannerEvents
|
||||
{
|
||||
public Func<ScannerState, Task>? OnStateUpdate { get; set; }
|
||||
}
|
||||
14
src/Discovery.Core/Models/ScannerOptions.cs
Normal file
14
src/Discovery.Core/Models/ScannerOptions.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Discovery.Models;
|
||||
|
||||
public class ScannerOptions
|
||||
{
|
||||
public int Concurrency { get; set; } = 128;
|
||||
public int Ttl { get; set; } = 128;
|
||||
public bool DontFragment { get; set; } = true;
|
||||
public bool ResolveAll { get; set; } = true;
|
||||
public TimeSpan ArpTimeout { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||
public TimeSpan PingTimeout { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||
public List<IPAddress>? Nameservers { get; set; }
|
||||
}
|
||||
15
src/Discovery.Core/Models/ScannerState.cs
Normal file
15
src/Discovery.Core/Models/ScannerState.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
namespace Discovery.Models;
|
||||
|
||||
public class ScannerState
|
||||
{
|
||||
public ScanOperation Operation { get; set; } = ScanOperation.Idle;
|
||||
public TimeSpan Elapsed { get; set; } = TimeSpan.Zero;
|
||||
public double Progress { get; set; } = 0;
|
||||
}
|
||||
|
||||
public enum ScanOperation
|
||||
{
|
||||
Idle,
|
||||
Scanning,
|
||||
Aborted
|
||||
}
|
||||
190
src/Discovery.Core/Models/SortableObservableCollection.cs
Normal file
190
src/Discovery.Core/Models/SortableObservableCollection.cs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Discovery.Models;
|
||||
|
||||
public class SynchronizedCollection<T> : SortableObservableCollection<T>
|
||||
{
|
||||
private readonly SynchronizationContext _context;
|
||||
|
||||
public SynchronizedCollection(Func<T, int> indexSelector, SynchronizationContext? context) : base(indexSelector)
|
||||
{
|
||||
_context = context ?? new SynchronizationContext();
|
||||
}
|
||||
|
||||
public SynchronizedCollection(IEnumerable<T> collection, Func<T, int> indexSelector, SynchronizationContext? context) : base(collection, indexSelector)
|
||||
{
|
||||
_context = context ?? new SynchronizationContext();
|
||||
}
|
||||
|
||||
public override void Sort()
|
||||
{
|
||||
base.Sort();
|
||||
|
||||
if (SynchronizationContext.Current == _context)
|
||||
{
|
||||
base.Sort();
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.Send(_ => base.Sort(), null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void InsertItem(int index, T item)
|
||||
{
|
||||
if (SynchronizationContext.Current == _context)
|
||||
{
|
||||
base.InsertItem(index, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.Send(_ => base.InsertItem(index, item), null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveItem(int index)
|
||||
{
|
||||
if (SynchronizationContext.Current == _context)
|
||||
{
|
||||
base.RemoveItem(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.Send(_ => base.RemoveItem(index), null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ClearItems()
|
||||
{
|
||||
if (SynchronizationContext.Current == _context)
|
||||
{
|
||||
base.ClearItems();
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.Send(_ => base.ClearItems(), null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (SynchronizationContext.Current == _context)
|
||||
{
|
||||
base.OnItemPropertyChanged(sender, e);
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.Send(_ => base.OnItemPropertyChanged(sender, e), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SortableObservableCollection<T> : ObservableCollection<T>
|
||||
{
|
||||
public List<T> InternalList => _items;
|
||||
|
||||
public Func<T, bool>? Filter
|
||||
{
|
||||
get => _filter;
|
||||
set
|
||||
{
|
||||
_filter = value;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
|
||||
private Func<T, bool>? _filter;
|
||||
|
||||
private readonly Func<T, int> _indexSelector;
|
||||
private readonly List<T> _items = [];
|
||||
|
||||
public SortableObservableCollection(Func<T, int> indexSelector)
|
||||
{
|
||||
_indexSelector = indexSelector;
|
||||
}
|
||||
|
||||
public SortableObservableCollection(IEnumerable<T> collection, Func<T, int> indexSelector) : base(collection)
|
||||
{
|
||||
_indexSelector = indexSelector;
|
||||
Sort();
|
||||
}
|
||||
|
||||
public virtual void Sort()
|
||||
{
|
||||
var sortedList = this.ToList();
|
||||
var ordered = sortedList.OrderBy(_indexSelector).ToList();
|
||||
|
||||
// Rearrange the collection to reflect the sorted order
|
||||
for (int i = 0; i < ordered.Count; i++)
|
||||
{
|
||||
Move(IndexOf(ordered[i]), i);
|
||||
}
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
protected override void InsertItem(int index, T item)
|
||||
{
|
||||
_items.Insert(index, item);
|
||||
base.InsertItem(index, item);
|
||||
|
||||
if (item is INotifyPropertyChanged npc)
|
||||
{
|
||||
npc.PropertyChanged += Item_PropertyChanged;
|
||||
}
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
protected override void RemoveItem(int index)
|
||||
{
|
||||
_items.RemoveAt(index);
|
||||
|
||||
if (_items[index] is INotifyPropertyChanged npc)
|
||||
{
|
||||
npc.PropertyChanged -= Item_PropertyChanged;
|
||||
}
|
||||
|
||||
//base.RemoveItem(index);
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
protected override void ClearItems()
|
||||
{
|
||||
_items.Clear();
|
||||
|
||||
Update();
|
||||
return;
|
||||
}
|
||||
|
||||
protected virtual void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
//Console.WriteLine($"Item property changed: {e.PropertyName}");
|
||||
Update();
|
||||
}
|
||||
|
||||
private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
OnItemPropertyChanged(sender, e);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var sorted = _items
|
||||
.Where(p => _filter == null || _filter(p))
|
||||
.OrderBy(_indexSelector)
|
||||
.ToList();
|
||||
|
||||
base.Items.Clear();
|
||||
|
||||
for (int i = 0; i < sorted.Count; i++)
|
||||
{
|
||||
base.Items.Insert(i, sorted[i]);
|
||||
}
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
}
|
||||
102
src/Discovery.Core/Models/Target.cs
Normal file
102
src/Discovery.Core/Models/Target.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.ComponentModel;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace Discovery.Models;
|
||||
|
||||
public partial class Target(int index, IPAddress ipAddress) : INotifyPropertyChanged
|
||||
{
|
||||
public int Index { get; } = index;
|
||||
public IPAddress IPAddress => _ipAddress;
|
||||
public string Address => _ipAddress.ToString();
|
||||
public bool Online => IsOnline();
|
||||
|
||||
public string? Hostname
|
||||
{
|
||||
get => _hostname;
|
||||
set
|
||||
{
|
||||
_hostname = value;
|
||||
OnPropertyChanged(nameof(Hostname));
|
||||
}
|
||||
}
|
||||
public string? Mac
|
||||
{
|
||||
get => _mac;
|
||||
set
|
||||
{
|
||||
_mac = value;
|
||||
OnPropertyChanged(nameof(Mac));
|
||||
}
|
||||
}
|
||||
public string? Vendor
|
||||
{
|
||||
get => _vendor;
|
||||
set
|
||||
{
|
||||
_vendor = value;
|
||||
OnPropertyChanged(nameof(Vendor));
|
||||
}
|
||||
}
|
||||
public string? Ports
|
||||
{
|
||||
get => _ports;
|
||||
set
|
||||
{
|
||||
_ports = value;
|
||||
OnPropertyChanged(nameof(Ports));
|
||||
OnPropertyChanged(nameof(Online));
|
||||
}
|
||||
}
|
||||
|
||||
public PingReply? Ping
|
||||
{
|
||||
get => _ping;
|
||||
set
|
||||
{
|
||||
_ping = value;
|
||||
OnPropertyChanged(nameof(Ping));
|
||||
OnPropertyChanged(nameof(Online));
|
||||
}
|
||||
}
|
||||
|
||||
public bool Arp
|
||||
{
|
||||
get => _arp;
|
||||
set
|
||||
{
|
||||
_arp = value;
|
||||
OnPropertyChanged(nameof(Arp));
|
||||
OnPropertyChanged(nameof(Online));
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private readonly IPAddress _ipAddress = ipAddress;
|
||||
private bool _arp;
|
||||
private PingReply? _ping;
|
||||
private string? _hostname;
|
||||
private string? _mac;
|
||||
private string? _vendor;
|
||||
private string? _ports;
|
||||
|
||||
protected virtual void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private bool IsOnline()
|
||||
{
|
||||
if (Ping?.Status == IPStatus.Success)
|
||||
return true;
|
||||
|
||||
if (Arp == true)
|
||||
return true;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Ports))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
15
src/Discovery.Core/Models/Vendor.cs
Normal file
15
src/Discovery.Core/Models/Vendor.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Discovery.Models;
|
||||
|
||||
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
|
||||
[JsonSerializable(typeof(Vendor[]))]
|
||||
public partial class VendorsJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
public class Vendor
|
||||
{
|
||||
public string? MacPrefix { get; set; }
|
||||
public string? VendorName { get; set; }
|
||||
}
|
||||
113
src/Discovery.Core/Services/Exporter.cs
Normal file
113
src/Discovery.Core/Services/Exporter.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using Discovery.Models;
|
||||
using System.Text;
|
||||
|
||||
namespace Discovery.Services;
|
||||
|
||||
public static class Exporter
|
||||
{
|
||||
public static async Task ExportAsCsvAsync(IEnumerable<Target> targets, string file, string delimiter = ";")
|
||||
{
|
||||
using var writer = new StreamWriter(file);
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine(string.Join(delimiter,
|
||||
nameof(Target.Address),
|
||||
nameof(Target.Hostname),
|
||||
nameof(Target.Mac),
|
||||
nameof(Target.Vendor),
|
||||
nameof(Target.Online)));
|
||||
|
||||
foreach (var item in targets)
|
||||
{
|
||||
if (item == null || item.Address == null)
|
||||
continue;
|
||||
|
||||
sb.AppendLine(string.Join(delimiter,
|
||||
item.Address,
|
||||
item.Hostname,
|
||||
item.Mac,
|
||||
item.Vendor,
|
||||
item.Online));
|
||||
}
|
||||
|
||||
await writer.WriteAsync(sb);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
|
||||
public static async Task ExportAsHtmlAsync(IEnumerable<Target> targets, string file, string delimiter = ";")
|
||||
{
|
||||
using var writer = new StreamWriter(file);
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var csvWriter = new StringWriter();
|
||||
|
||||
await csvWriter.WriteLineAsync(string.Join(delimiter,
|
||||
nameof(Target.Address),
|
||||
nameof(Target.Hostname),
|
||||
nameof(Target.Mac),
|
||||
nameof(Target.Vendor),
|
||||
nameof(Target.Online)));
|
||||
|
||||
foreach (var item in targets)
|
||||
{
|
||||
if (item == null || item.Address == null)
|
||||
continue;
|
||||
|
||||
await csvWriter.WriteLineAsync(string.Join(delimiter,
|
||||
item.Address,
|
||||
item.Hostname,
|
||||
item.Mac,
|
||||
item.Vendor,
|
||||
item.Online));
|
||||
}
|
||||
|
||||
await writer.WriteAsync(HmtlTableHelper(csvWriter.ToString(), delimiter));
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
|
||||
private static string HmtlTableHelper(string csv, string separator)
|
||||
{
|
||||
var html = string.Empty;
|
||||
html += "<!DOCTYPE html>";
|
||||
html += "<html>";
|
||||
html += "<head>";
|
||||
html += "<style>";
|
||||
html += "#style#";
|
||||
html += "</style>";
|
||||
html += "</head>";
|
||||
html += "<body>";
|
||||
html += "<table>";
|
||||
html += "#table#";
|
||||
html += "</table>";
|
||||
html += "</body>";
|
||||
html += "</html>";
|
||||
|
||||
var style = string.Empty;
|
||||
style += string.Empty;
|
||||
style += "body { font-family: \"Open Sans\", sans-serif; line-height: 1.25; }";
|
||||
style += "table { border: 1px solid #ccc; border-collapse: collapse; margin: 0; padding: 0; width: 100%; table-layout: fixed; } table caption { font-size: 1.5em; margin: .5em 0 .75em; } table tr { background-color: #f8f8f8; border: 1px solid #ddd; padding: .35em; } table th, table td { padding: .625em; text-align: center; } table th { font-size: .85em; letter-spacing: .1em; text-transform: uppercase; }";
|
||||
|
||||
html = html.Replace("#style#", style);
|
||||
|
||||
var table = string.Empty;
|
||||
|
||||
foreach (var line in csv.Split('\n'))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
table += "<tr>";
|
||||
|
||||
foreach (var prop in line.Split(separator))
|
||||
{
|
||||
table += "<td>";
|
||||
table += prop;
|
||||
table += "</td>";
|
||||
}
|
||||
|
||||
table += "</tr>";
|
||||
}
|
||||
|
||||
return html.Replace("#table#", table);
|
||||
}
|
||||
}
|
||||
295
src/Discovery.Core/Services/Scanner.cs
Normal file
295
src/Discovery.Core/Services/Scanner.cs
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
using Discovery.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Discovery.Services;
|
||||
|
||||
public partial class Scanner<T> where T : Target
|
||||
{
|
||||
public ScannerState State => _state;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private readonly ScannerState _state = new();
|
||||
private readonly static Dictionary<string, string>? _vendors = BuildVendorDictionary();
|
||||
|
||||
public async Task<ScannerState> ScanAsync(
|
||||
SynchronizedCollection<T> targets,
|
||||
ScannerOptions options,
|
||||
ScannerEvents events,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// cancel if already running
|
||||
if (_state.Operation == ScanOperation.Scanning && _cts != null)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
return _state;
|
||||
}
|
||||
|
||||
// renew state
|
||||
_state.Operation = ScanOperation.Scanning;
|
||||
_state.Progress = 0;
|
||||
|
||||
// set cts
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
var orderedTargets = targets.InternalList.OrderBy(p => p.Index);
|
||||
var start = Stopwatch.GetTimestamp();
|
||||
|
||||
try
|
||||
{
|
||||
var tasks = new List<Task>(targets.InternalList.Count);
|
||||
var con = new SemaphoreSlim(options.Concurrency);
|
||||
|
||||
var pingOptions = new PingOptions
|
||||
{
|
||||
DontFragment = options.DontFragment,
|
||||
Ttl = options.Ttl
|
||||
};
|
||||
|
||||
foreach (var target in targets.InternalList)
|
||||
{
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await con.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
target.Ping = await PingAsync(target.IPAddress, options.PingTimeout, null, pingOptions, cancellationToken).ConfigureAwait(false);
|
||||
target.Mac = await ArpAsync(target.IPAddress, options.ArpTimeout, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (target.Mac != null)
|
||||
{
|
||||
target.Arp = true;
|
||||
target.Vendor = ResolveVendor(target.Mac!);
|
||||
}
|
||||
|
||||
// local resolve fix
|
||||
string? sAddress = null;
|
||||
if (TryGetPrimaryInterface(out var pInterface, out var pAddress))
|
||||
sAddress = pAddress?.ToString();
|
||||
|
||||
if (target.Address == sAddress)
|
||||
{
|
||||
target.Hostname = Dns.GetHostName();
|
||||
}
|
||||
else if (options.Nameservers == null)
|
||||
{
|
||||
target.Hostname = "N/A";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (target.Online || (!target.Online && options.ResolveAll))
|
||||
{
|
||||
var result = await ResolveHostnameAsync(target.IPAddress, options.Nameservers, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null)
|
||||
target.Hostname = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
finally
|
||||
{
|
||||
_state.Progress += (100 / (double)targets.InternalList.Count);
|
||||
|
||||
if (events.OnStateUpdate != null)
|
||||
await events.OnStateUpdate(_state);
|
||||
|
||||
con.Release();
|
||||
}
|
||||
}, cancellationToken));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
_state.Operation = ScanOperation.Idle;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_state.Operation = ScanOperation.Aborted;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_state.Progress = 100;
|
||||
_state.Elapsed = Stopwatch.GetElapsedTime(start);
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
return _state;
|
||||
}
|
||||
|
||||
public async Task CancelAsync()
|
||||
{
|
||||
if (_cts == null)
|
||||
return;
|
||||
|
||||
await _cts.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
_state.Operation = ScanOperation.Aborted;
|
||||
}
|
||||
|
||||
public bool TryGetPrimaryInterface(out NetworkInterface? pInterface, out IPAddress? pAddress)
|
||||
{
|
||||
pInterface = null;
|
||||
pAddress = null;
|
||||
|
||||
try
|
||||
{
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
|
||||
socket.Connect("8.8.8.8", 65530);
|
||||
|
||||
var ep = socket.LocalEndPoint as IPEndPoint;
|
||||
|
||||
if (ep?.Address == null)
|
||||
return false;
|
||||
|
||||
pAddress = ep.Address;
|
||||
|
||||
pInterface = NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.FirstOrDefault(p => p
|
||||
.GetIPProperties().UnicastAddresses.Any(x => x.Address.ToString() == ep.Address.ToString()));
|
||||
|
||||
if (pInterface == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkInfo? GetPrimaryNetwork()
|
||||
{
|
||||
if (!TryGetPrimaryInterface(out var pInterface, out var pAddress))
|
||||
return null;
|
||||
|
||||
var pUnicast = pInterface!.GetIPProperties().UnicastAddresses.Single(p => p.Address.ToString() == pAddress!.ToString());
|
||||
|
||||
if (!IPNetwork2.TryParse(pAddress!, pUnicast!.IPv4Mask, out var net))
|
||||
return null;
|
||||
|
||||
var nameservers = pInterface!.GetIPProperties().DnsAddresses;
|
||||
|
||||
return new NetworkInfo(pAddress!, net.Network, net.Cidr, [.. nameservers]);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildVendorDictionary()
|
||||
{
|
||||
var dictionary = new Dictionary<string, string>();
|
||||
|
||||
using var fileStream = new FileStream("vendors.json", FileMode.Open, FileAccess.Read);
|
||||
|
||||
var vendors = JsonSerializer.Deserialize(
|
||||
fileStream,
|
||||
VendorsJsonContext.Default.VendorArray
|
||||
) ?? throw new InvalidOperationException("vendors.json");
|
||||
|
||||
foreach (var item in vendors)
|
||||
{
|
||||
if (item is null)
|
||||
continue;
|
||||
|
||||
if (item.MacPrefix == null || item.VendorName == null)
|
||||
continue;
|
||||
|
||||
dictionary.Add(item.MacPrefix, item.VendorName);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static async Task<PingReply?> PingAsync(IPAddress address, TimeSpan timeout, byte[]? buffer, PingOptions? options, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return null;
|
||||
|
||||
using var ping = new Ping();
|
||||
var reply = await ping.SendPingAsync(address, timeout, buffer, options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
ping.SendAsyncCancel();
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
private static async Task<string?> ArpAsync(IPAddress address, TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return null;
|
||||
if (address.AddressFamily != AddressFamily.InterNetwork) return null; // IPv4 only
|
||||
|
||||
// 1) Try ARP cache (GetIpNetTable)
|
||||
if (WinArpInterop.TryGetFromArpCache(address, out var mac))
|
||||
return mac;
|
||||
|
||||
// 2) Active probe on a dedicated thread; still honor the timeout
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
var probeTask = Task.Factory.StartNew(
|
||||
() => WinArpInterop.ProbeWithSendArp(address),
|
||||
linked.Token,
|
||||
TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
|
||||
TaskScheduler.Default);
|
||||
|
||||
var completed = await Task.WhenAny(probeTask, Task.Delay(timeout, linked.Token)).ConfigureAwait(false);
|
||||
if (completed != probeTask) return null;
|
||||
|
||||
linked.Cancel(); // cancel the delay if still pending
|
||||
return await probeTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<string?> ResolveHostnameAsync(IPAddress address, List<IPAddress> nameservers, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return null;
|
||||
|
||||
string? hostname = null;
|
||||
|
||||
for (int i = 0; i < nameservers.Count; i++)
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "nslookup",
|
||||
Arguments = $"{address} {nameservers[i]}",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput
|
||||
.ReadToEndAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var match = NameRegex().Match(output);
|
||||
if (match.Success)
|
||||
hostname = match.Groups["Name"].Value;
|
||||
|
||||
if (hostname != null)
|
||||
break;
|
||||
}
|
||||
|
||||
return hostname;
|
||||
}
|
||||
|
||||
private static string? ResolveVendor(string mac)
|
||||
{
|
||||
if (!_vendors!.TryGetValue(mac[..8].ToUpperInvariant(), out string? vendor))
|
||||
return null;
|
||||
|
||||
return vendor;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"Name:\s*(?<Name>[^\s]+)")]
|
||||
private static partial Regex NameRegex();
|
||||
}
|
||||
106
src/Discovery.Core/Services/WinArpInterop.cs
Normal file
106
src/Discovery.Core/Services/WinArpInterop.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Discovery.Services;
|
||||
|
||||
internal static partial class WinArpInterop
|
||||
{
|
||||
public static bool TryGetFromArpCache(IPAddress ip, out string? mac)
|
||||
{
|
||||
mac = null;
|
||||
|
||||
int size = 0;
|
||||
// First call to get required size
|
||||
int err = GetIpNetTable(IntPtr.Zero, ref size, bOrder: false);
|
||||
if (err != ERROR_INSUFFICIENT_BUFFER || size <= 0)
|
||||
return false;
|
||||
|
||||
IntPtr buf = Marshal.AllocHGlobal(size);
|
||||
try
|
||||
{
|
||||
err = GetIpNetTable(buf, ref size, bOrder: false);
|
||||
if (err != NO_ERROR) return false;
|
||||
|
||||
// Layout: DWORD dwNumEntries; then dwNumEntries * MIB_IPNETROW
|
||||
int num = Marshal.ReadInt32(buf);
|
||||
IntPtr rowPtr = buf + sizeof(int);
|
||||
int rowSize = Marshal.SizeOf<MIB_IPNETROW>();
|
||||
|
||||
int target = BitConverter.ToInt32(ip.GetAddressBytes(), 0);
|
||||
|
||||
for (int i = 0; i < num; i++)
|
||||
{
|
||||
var row = Marshal.PtrToStructure<MIB_IPNETROW>(rowPtr);
|
||||
if (row.dwAddr == target && row.dwPhysAddrLen >= 6)
|
||||
{
|
||||
// Accept dynamic/valid types: 3=dynamic, 4=static (2=incomplete)
|
||||
if (row.dwType == MIB_IPNET_TYPE_DYNAMIC || row.dwType == MIB_IPNET_TYPE_STATIC)
|
||||
{
|
||||
mac = FormatMac(row.bPhysAddr.AsSpan(0, 6));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
rowPtr += rowSize;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buf);
|
||||
}
|
||||
}
|
||||
|
||||
public static string? ProbeWithSendArp(IPAddress ip)
|
||||
{
|
||||
Span<byte> mac = stackalloc byte[6];
|
||||
uint len = 6;
|
||||
// SendARP expects dest IPv4 as DWORD (same byte order as BitConverter.ToInt32)
|
||||
int dest = BitConverter.ToInt32(ip.GetAddressBytes(), 0);
|
||||
int r = SendARP(dest, 0, ref MemoryMarshal.GetReference(mac), ref len);
|
||||
if (r != 0 || len < 6) return null;
|
||||
return FormatMac(mac);
|
||||
}
|
||||
|
||||
private static string FormatMac(ReadOnlySpan<byte> mac)
|
||||
{
|
||||
// Fast formatter without intermediate strings
|
||||
Span<char> chars = stackalloc char[17];
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
byte b = mac[i];
|
||||
chars[i * 3] = GetHexUpper(b >> 4);
|
||||
chars[i * 3 + 1] = GetHexUpper(b & 0xF);
|
||||
if (i != 5) chars[i * 3 + 2] = ':';
|
||||
}
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
private static char GetHexUpper(int v) => (char)(v < 10 ? '0' + v : 'A' + (v - 10));
|
||||
|
||||
private const int NO_ERROR = 0;
|
||||
private const int ERROR_INSUFFICIENT_BUFFER = 122;
|
||||
|
||||
private const int MIB_IPNET_TYPE_OTHER = 1;
|
||||
private const int MIB_IPNET_TYPE_INCOMPLETE = 2;
|
||||
private const int MIB_IPNET_TYPE_DYNAMIC = 3;
|
||||
private const int MIB_IPNET_TYPE_STATIC = 4;
|
||||
|
||||
[LibraryImport("iphlpapi.dll", SetLastError = true)]
|
||||
private static partial int GetIpNetTable(IntPtr pIpNetTable, ref int pdwSize, [MarshalAs(UnmanagedType.Bool)] bool bOrder);
|
||||
|
||||
[LibraryImport("iphlpapi.dll")]
|
||||
private static partial int SendARP(int DestIP, int SrcIP, ref byte pMacAddr, ref uint PhyAddrLen);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MIB_IPNETROW
|
||||
{
|
||||
public int dwIndex;
|
||||
public int dwPhysAddrLen;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
|
||||
public byte[] bPhysAddr;
|
||||
|
||||
public int dwAddr;
|
||||
public int dwType;
|
||||
}
|
||||
}
|
||||
1
src/Discovery.Core/vendors.json
Normal file
1
src/Discovery.Core/vendors.json
Normal file
File diff suppressed because one or more lines are too long
47
src/Discovery.Desktop/Discovery.Desktop.csproj
Normal file
47
src/Discovery.Desktop/Discovery.Desktop.csproj
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Discovery</AssemblyName>
|
||||
<RootNamespace>Discovery.Desktop</RootNamespace>
|
||||
<Product>Discovery</Product>
|
||||
<Authors>Kevin Kai Berthold</Authors>
|
||||
<Copyright>Webmatic GmbH</Copyright>
|
||||
<AssemblyVersion>2025.3.14.0</AssemblyVersion>
|
||||
<PackageVersion>2025.3.14.0</PackageVersion>
|
||||
<FileVersion>2025.3.14.0</FileVersion>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
|
||||
|
||||
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
|
||||
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
|
||||
<EnableAotAnalyzer>true</EnableAotAnalyzer>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<GenerateAssemblyInfo>True</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Avalonia">
|
||||
<AvaloniaUseCompiledBindingsByDefault>True</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Discovery.Avalonia\Discovery.Avalonia.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
18
src/Discovery.Desktop/Program.cs
Normal file
18
src/Discovery.Desktop/Program.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Avalonia;
|
||||
using Discovery.Avalonia;
|
||||
|
||||
namespace Discovery.Desktop;
|
||||
|
||||
class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
}
|
||||
21
src/Discovery.Desktop/Properties/PublishProfiles/aot.pubxml
Normal file
21
src/Discovery.Desktop/Properties/PublishProfiles/aot.pubxml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net9.0\publish\aot</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishAot>true</PublishAot>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
<TrimMode>copyused</TrimMode>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
20
src/Discovery.Desktop/Properties/PublishProfiles/gc.pubxml
Normal file
20
src/Discovery.Desktop/Properties/PublishProfiles/gc.pubxml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net9.0\publish\gc</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
18
src/Discovery.Desktop/app.manifest
Normal file
18
src/Discovery.Desktop/app.manifest
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embeded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="AvaloniaTest.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
BIN
src/Discovery.Desktop/icon.ico
Normal file
BIN
src/Discovery.Desktop/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
9
test/Discovery.Loader.Avalonia/App.axaml
Normal file
9
test/Discovery.Loader.Avalonia/App.axaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Discovery.Loader.App"
|
||||
RequestedThemeVariant="Light">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
135
test/Discovery.Loader.Avalonia/App.axaml.cs
Normal file
135
test/Discovery.Loader.Avalonia/App.axaml.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Discovery.Loader.Models;
|
||||
using Discovery.Loader.Services;
|
||||
using Discovery.Loader.ViewModels;
|
||||
using Discovery.Loader.Windows;
|
||||
|
||||
namespace Discovery.Loader;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private UpdateService _updateService = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override async void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
BindingPlugins.DataValidators.RemoveAt(0);
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var installed = _updateService.IsInstalled();
|
||||
|
||||
|
||||
|
||||
// update-check
|
||||
Update? update = null;
|
||||
|
||||
try
|
||||
{
|
||||
update = await _updateService.GetUpdateAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// if not installed (need update to install)
|
||||
if (!installed)
|
||||
{
|
||||
if (update == null)
|
||||
{
|
||||
// set window
|
||||
var vm = new MainViewModel();
|
||||
var mw = new MainWindow { DataContext = vm };
|
||||
desktop.MainWindow = mw;
|
||||
|
||||
// display major error
|
||||
mw.Show();
|
||||
await vm.ErrorAsync("Install failed (Source unavailable)");
|
||||
mw.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
// set window
|
||||
var vm = new MainViewModel();
|
||||
var mw = new MainWindow { DataContext = vm };
|
||||
desktop.MainWindow = mw;
|
||||
|
||||
// display install progress
|
||||
mw.Show();
|
||||
await vm.InstallAsync(_updateService, update);
|
||||
mw.Close();
|
||||
|
||||
// start
|
||||
_updateService.StartExecutable();
|
||||
}
|
||||
}
|
||||
// if installed (should update if needed)
|
||||
else
|
||||
{
|
||||
if (update == null)
|
||||
{
|
||||
// set window
|
||||
var vm = new MainViewModel();
|
||||
var mw = new MainWindow { DataContext = vm };
|
||||
desktop.MainWindow = mw;
|
||||
|
||||
// display cant fetch updates, start afterwards
|
||||
mw.Show();
|
||||
await vm.ErrorAsync("Failed to fetch updates.");
|
||||
mw.Close();
|
||||
|
||||
// start anyway
|
||||
_updateService.StartExecutable();
|
||||
}
|
||||
else
|
||||
{
|
||||
// compare versions
|
||||
var isUpdated = _updateService.CompareVersion(update);
|
||||
if (!isUpdated)
|
||||
{
|
||||
// set window
|
||||
var vm = new MainViewModel();
|
||||
var mw = new MainWindow { DataContext = vm };
|
||||
desktop.MainWindow = mw;
|
||||
|
||||
// display update
|
||||
mw.Show();
|
||||
await vm.UpdateAsync(_updateService, update);
|
||||
mw.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
// on current-version just start
|
||||
_updateService.StartExecutable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
//mw.Close();
|
||||
|
||||
//if (_updateService.IsInstalled())
|
||||
// _updateService.StartExecutable();
|
||||
|
||||
desktop.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
BIN
test/Discovery.Loader.Avalonia/Assets/icon.ico
Normal file
BIN
test/Discovery.Loader.Avalonia/Assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Discovery.Loader</RootNamespace>
|
||||
<Product>Discovery</Product>
|
||||
<Authors>Kevin Kai Berthold</Authors>
|
||||
<Copyright>Webmatic GmbH</Copyright>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.6" />
|
||||
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.6" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
18
test/Discovery.Loader.Avalonia/Models/Update.cs
Normal file
18
test/Discovery.Loader.Avalonia/Models/Update.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Discovery.Loader.Models;
|
||||
|
||||
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(Update))]
|
||||
public partial class UpdateContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
public class Update
|
||||
{
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { get; set; }
|
||||
|
||||
[JsonPropertyName("link")]
|
||||
public string? Link { get; set; }
|
||||
}
|
||||
167
test/Discovery.Loader.Avalonia/Services/UpdateService.cs
Normal file
167
test/Discovery.Loader.Avalonia/Services/UpdateService.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
using Discovery.Loader.Models;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace Discovery.Loader.Services;
|
||||
|
||||
public class UpdateService
|
||||
{
|
||||
public DirectoryInfo AppDir { get; private set; }
|
||||
|
||||
public UpdateService()
|
||||
{
|
||||
var localApps = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
|
||||
|
||||
AppDir = new(Path.Combine(localApps.FullName + @"\Discovery"));
|
||||
}
|
||||
|
||||
public bool IsInstalled()
|
||||
{
|
||||
if (!AppDir.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AppDir.EnumerateFiles().Where(p => p.Name.Contains("Discovery.exe", StringComparison.InvariantCultureIgnoreCase)).Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Update?> GetUpdateAsync()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
|
||||
var update = await httpClient.GetFromJsonAsync(
|
||||
"https://raw.githubusercontent.com/vaitr/discovery/refs/heads/main/version.json",
|
||||
UpdateContext.Default.Update,
|
||||
default);
|
||||
|
||||
if (update == null)
|
||||
{
|
||||
Console.WriteLine("Source not available");
|
||||
return null;
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
public bool CompareVersion(Update update)
|
||||
{
|
||||
Console.WriteLine(update.Version);
|
||||
Console.WriteLine(update.Link);
|
||||
|
||||
var mainExecutable = new FileInfo(Path.Combine(AppDir.FullName + @"/discovery.exe"));
|
||||
if (!mainExecutable.Exists)
|
||||
return false;
|
||||
|
||||
var currentVersion = FileVersionInfo.GetVersionInfo(mainExecutable.FullName).FileVersion;
|
||||
|
||||
var vCurrent = Version.Parse(currentVersion!);
|
||||
var vUpdate = Version.Parse(update.Version);
|
||||
|
||||
if (vUpdate > vCurrent)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadAsync(Update update, Func<double, Task>? onProgress)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.Timeout = TimeSpan.FromMinutes(3);
|
||||
|
||||
using var fileStream = new FileStream(Path.Combine(AppDir.FullName + @"/update.zip"), FileMode.Create);
|
||||
|
||||
// Get the http headers first to examine the content length
|
||||
using var response = await httpClient.GetAsync(update.Link, HttpCompletionOption.ResponseHeadersRead);
|
||||
var contentLength = response.Content.Headers.ContentLength;
|
||||
|
||||
using var download = await response.Content.ReadAsStreamAsync(default);
|
||||
|
||||
if (onProgress == null || !contentLength.HasValue)
|
||||
{
|
||||
await download.CopyToAsync(fileStream);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
|
||||
var relativeProgress = new Progress<long>(async (totalBytes) =>
|
||||
{
|
||||
Console.WriteLine(totalBytes + " / " + contentLength.Value + " / " + (float)totalBytes / contentLength.Value);
|
||||
await onProgress((float)totalBytes / contentLength.Value);
|
||||
});
|
||||
|
||||
// Use extension method to report progress while downloading
|
||||
await download.CopyToAsync(fileStream, 81920, relativeProgress, default);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Extract()
|
||||
{
|
||||
var archivePath = Path.Combine(AppDir.FullName + @"/update.zip");
|
||||
|
||||
using (var archive = ZipFile.OpenRead(archivePath))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var file = new FileInfo(Path.Combine(AppDir.FullName, entry.Name));
|
||||
if (file.Exists)
|
||||
file.Delete();
|
||||
|
||||
entry.ExtractToFile(file.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
File.Delete(archivePath);
|
||||
}
|
||||
|
||||
public void StartExecutable()
|
||||
{
|
||||
var exec = new FileInfo(Path.Combine(AppDir.FullName + @"/discovery.exe"));
|
||||
_ = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = true,
|
||||
FileName = exec.FullName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class StreamExtensions
|
||||
{
|
||||
public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
if (!source.CanRead)
|
||||
throw new ArgumentException("Has to be readable", nameof(source));
|
||||
if (destination == null)
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
if (!destination.CanWrite)
|
||||
throw new ArgumentException("Has to be writable", nameof(destination));
|
||||
if (bufferSize < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
||||
|
||||
var buffer = new byte[bufferSize];
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
|
||||
totalBytesRead += bytesRead;
|
||||
progress?.Report(totalBytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
test/Discovery.Loader.Avalonia/ViewModels/MainViewModel.cs
Normal file
105
test/Discovery.Loader.Avalonia/ViewModels/MainViewModel.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using Avalonia.Threading;
|
||||
using Discovery.Loader.Models;
|
||||
using Discovery.Loader.Services;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Discovery.Loader.ViewModels;
|
||||
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
public string? Text
|
||||
{
|
||||
get => _text;
|
||||
set => this.RaiseAndSetIfChanged(ref _text, value);
|
||||
}
|
||||
|
||||
private string? _text;
|
||||
|
||||
public double Progress
|
||||
{
|
||||
get => _progress;
|
||||
set => this.RaiseAndSetIfChanged(ref _progress, value);
|
||||
}
|
||||
|
||||
private double _progress;
|
||||
|
||||
public async Task InstallAsync(UpdateService updater, Update update)
|
||||
{
|
||||
if (!updater.AppDir.Exists)
|
||||
updater.AppDir.Create();
|
||||
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Text = "Install";
|
||||
});
|
||||
|
||||
var download = await updater.DownloadAsync(update, (p) => Task.Run(() =>
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Progress = p;
|
||||
}, DispatcherPriority.Default, default);
|
||||
}, default));
|
||||
|
||||
if (!download)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Text = "Failed to install";
|
||||
});
|
||||
|
||||
await Task.Delay(3000);
|
||||
return;
|
||||
}
|
||||
|
||||
updater.Extract();
|
||||
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(UpdateService updater, Update update)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Text = "Download Update";
|
||||
});
|
||||
|
||||
var download = await updater.DownloadAsync(update, (p) => Task.Run(() =>
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Progress = p;
|
||||
}, DispatcherPriority.Default, default);
|
||||
}, default));
|
||||
|
||||
if (!download)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Text = "Failed to download Update!";
|
||||
});
|
||||
|
||||
await Task.Delay(3000);
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Text = "Extract Update";
|
||||
});
|
||||
|
||||
updater.Extract();
|
||||
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
public async Task ErrorAsync(string message)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
Text = message;
|
||||
});
|
||||
|
||||
await Task.Delay(3000);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
using ReactiveUI;
|
||||
|
||||
namespace Discovery.Loader.ViewModels;
|
||||
|
||||
public class ViewModelBase : ReactiveObject
|
||||
{
|
||||
}
|
||||
33
test/Discovery.Loader.Avalonia/Views/MainView.axaml
Normal file
33
test/Discovery.Loader.Avalonia/Views/MainView.axaml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Discovery.Loader.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Discovery.Loader.Views.MainView"
|
||||
x:DataType="vm:MainViewModel"
|
||||
Width="300"
|
||||
Height="100">
|
||||
<Design.DataContext>
|
||||
<vm:MainViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,0,20"
|
||||
Text="{Binding Text, Mode=OneWay}"/>
|
||||
<ProgressBar Grid.Row="2"
|
||||
Margin="10,0,10,0"
|
||||
Minimum="0"
|
||||
Maximum="1"
|
||||
Value="{Binding Progress, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
test/Discovery.Loader.Avalonia/Views/MainView.axaml.cs
Normal file
11
test/Discovery.Loader.Avalonia/Views/MainView.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace Discovery.Loader.Views;
|
||||
|
||||
public partial class MainView : UserControl
|
||||
{
|
||||
public MainView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
20
test/Discovery.Loader.Avalonia/Windows/MainWindow.axaml
Normal file
20
test/Discovery.Loader.Avalonia/Windows/MainWindow.axaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:Discovery.Loader.Views"
|
||||
xmlns:vm="using:Discovery.Loader.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Discovery.Loader.Windows.MainWindow"
|
||||
Icon="/Assets/icon.ico"
|
||||
Title="Discovery"
|
||||
Width="300"
|
||||
Height="100"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="NoChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="-1">
|
||||
<views:MainView />
|
||||
</Window>
|
||||
14
test/Discovery.Loader.Avalonia/Windows/MainWindow.axaml.cs
Normal file
14
test/Discovery.Loader.Avalonia/Windows/MainWindow.axaml.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace Discovery.Loader.Windows;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public static MainWindow? Instance { get; set; }
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
Instance = this;
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
33
test/Discovery.Loader/Discovery.Loader.Desktop.csproj
Normal file
33
test/Discovery.Loader/Discovery.Loader.Desktop.csproj
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Discovery</AssemblyName>
|
||||
<RootNamespace>Discovery.Loader</RootNamespace>
|
||||
<Product>Discovery</Product>
|
||||
<Authors>Kevin Kai Berthold</Authors>
|
||||
<Copyright>Webmatic GmbH</Copyright>
|
||||
<AssemblyVersion>2024.1.25.0</AssemblyVersion>
|
||||
<PackageVersion>2024.1.25.0</PackageVersion>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Discovery.Loader.Avalonia\Discovery.Loader.Avalonia.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Remove="icon.ico" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
18
test/Discovery.Loader/Program.cs
Normal file
18
test/Discovery.Loader/Program.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Avalonia;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Discovery.Loader;
|
||||
|
||||
public class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
21
test/Discovery.Loader/Properties/PublishProfiles/aot.pubxml
Normal file
21
test/Discovery.Loader/Properties/PublishProfiles/aot.pubxml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net8.0\publish\aot</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishAot>true</PublishAot>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
<TrimMode>copyused</TrimMode>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
20
test/Discovery.Loader/Properties/PublishProfiles/jit.pubxml
Normal file
20
test/Discovery.Loader/Properties/PublishProfiles/jit.pubxml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net8.0\publish\jit</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
18
test/Discovery.Loader/app.manifest
Normal file
18
test/Discovery.Loader/app.manifest
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embeded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="AvaloniaTest.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
BIN
test/Discovery.Loader/icon.ico
Normal file
BIN
test/Discovery.Loader/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Loading…
Add table
Add a link
Reference in a new issue