migration

This commit is contained in:
Kevin Kai Berthold 2025-09-24 22:10:04 +02:00
parent 561881c9aa
commit f133c740e1
55 changed files with 2928 additions and 20 deletions

View 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>

View 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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -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>

View 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; }
}

View 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);
}
}
}

View 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);
}
}

View file

@ -0,0 +1,7 @@
using ReactiveUI;
namespace Discovery.Loader.ViewModels;
public class ViewModelBase : ReactiveObject
{
}

View 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>

View file

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Discovery.Loader.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
}

View 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>

View 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();
}
}

View 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>

View 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();
}

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB