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