Browse Source

Add updater dialog

pull/55/head
Ionite 1 year ago
parent
commit
ef0918117f
No known key found for this signature in database
  1. 5
      StabilityMatrix.Avalonia/App.axaml.cs
  2. 20
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  3. 11
      StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs
  4. 91
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  5. 22
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  6. 85
      StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml
  7. 18
      StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml.cs
  8. 3
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  9. 8
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

5
StabilityMatrix.Avalonia/App.axaml.cs

@ -44,6 +44,7 @@ using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using Application = Avalonia.Application; using Application = Avalonia.Application;
using LogLevel = Microsoft.Extensions.Logging.LogLevel; using LogLevel = Microsoft.Extensions.Logging.LogLevel;
@ -125,6 +126,7 @@ public partial class App : Application
services.AddTransient<SelectModelVersionViewModel>(); services.AddTransient<SelectModelVersionViewModel>();
services.AddTransient<SelectDataDirectoryViewModel>(); services.AddTransient<SelectDataDirectoryViewModel>();
services.AddTransient<LaunchOptionsViewModel>(); services.AddTransient<LaunchOptionsViewModel>();
services.AddSingleton<UpdateViewModel>();
// Other transients (usually sub view models) // Other transients (usually sub view models)
services.AddTransient<CheckpointFolder>(); services.AddTransient<CheckpointFolder>();
@ -144,6 +146,7 @@ public partial class App : Application
.Register(provider.GetRequiredService<SelectModelVersionViewModel>) .Register(provider.GetRequiredService<SelectModelVersionViewModel>)
.Register(provider.GetRequiredService<SelectDataDirectoryViewModel>) .Register(provider.GetRequiredService<SelectDataDirectoryViewModel>)
.Register(provider.GetRequiredService<LaunchOptionsViewModel>) .Register(provider.GetRequiredService<LaunchOptionsViewModel>)
.Register(provider.GetRequiredService<UpdateViewModel>)
.Register(provider.GetRequiredService<CheckpointFolder>) .Register(provider.GetRequiredService<CheckpointFolder>)
.Register(provider.GetRequiredService<CheckpointFile>) .Register(provider.GetRequiredService<CheckpointFile>)
.Register(provider.GetRequiredService<RefreshBadgeViewModel>) .Register(provider.GetRequiredService<RefreshBadgeViewModel>)
@ -163,6 +166,7 @@ public partial class App : Application
// Dialogs // Dialogs
services.AddTransient<SelectDataDirectoryDialog>(); services.AddTransient<SelectDataDirectoryDialog>();
services.AddTransient<LaunchOptionsDialog>(); services.AddTransient<LaunchOptionsDialog>();
services.AddTransient<UpdateDialog>();
// Controls // Controls
services.AddTransient<RefreshBadge>(); services.AddTransient<RefreshBadge>();
@ -198,6 +202,7 @@ public partial class App : Application
services.AddSingleton<IPrerequisiteHelper, PrerequisiteHelper>(); services.AddSingleton<IPrerequisiteHelper, PrerequisiteHelper>();
services.AddSingleton<INotificationService, NotificationService>(); services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IPyRunner, PyRunner>(); services.AddSingleton<IPyRunner, PyRunner>();
services.AddSingleton<IUpdateHelper, UpdateHelper>();
services.AddSingleton<MainWindowViewModel>(provider => services.AddSingleton<MainWindowViewModel>(provider =>
new MainWindowViewModel(provider.GetRequiredService<ISettingsManager>(), new MainWindowViewModel(provider.GetRequiredService<ISettingsManager>(),

20
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Net.Http;
using AvaloniaEdit.Utils; using AvaloniaEdit.Utils;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
@ -19,6 +20,7 @@ using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia.DesignData; namespace StabilityMatrix.Avalonia.DesignData;
@ -52,12 +54,17 @@ public static class DesignData
}); });
// General services // General services
services.AddLogging(); services.AddLogging()
services.AddSingleton<IPackageFactory, PackageFactory>() .AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>()
.AddSingleton<ModelFinder>();
// Mock services
services
.AddSingleton<INotificationService, MockNotificationService>() .AddSingleton<INotificationService, MockNotificationService>()
.AddSingleton<ISharedFolders, MockSharedFolders>() .AddSingleton<ISharedFolders, MockSharedFolders>()
.AddSingleton<IDownloadService, MockDownloadService>() .AddSingleton<IDownloadService, MockDownloadService>()
.AddSingleton<ModelFinder>(); .AddSingleton<IHttpClientFactory, MockHttpClientFactory>();
// Placeholder services that nobody should need during design time // Placeholder services that nobody should need during design time
services services
@ -203,6 +210,11 @@ public static class DesignData
new(new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))), new(new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))),
new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu", new ProgressReport(0.25f, "Extracting..."))) new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu", new ProgressReport(0.25f, "Extracting...")))
}; };
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.UpdateText =
$"Stability Matrix v2.0.1 is now available! You currently have v2.0.0. Would you like to update now?";
UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature";
} }
public static MainWindowViewModel MainWindowViewModel { get; } public static MainWindowViewModel MainWindowViewModel { get; }
@ -225,4 +237,6 @@ public static class DesignData
{ {
State = ProgressState.Success State = ProgressState.Success
}; };
public static UpdateViewModel UpdateViewModel { get; }
} }

11
StabilityMatrix.Avalonia/DesignData/MockHttpClientFactory.cs

@ -0,0 +1,11 @@
using System.Net.Http;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
throw new System.NotImplementedException();
}
}

91
StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs

@ -0,0 +1,91 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(UpdateDialog))]
public partial class UpdateViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IHttpClientFactory httpClientFactory;
private readonly IUpdateHelper updateHelper;
[ObservableProperty] private bool isUpdateAvailable;
[ObservableProperty] private UpdateInfo? updateInfo;
[ObservableProperty] private string? releaseNotes;
[ObservableProperty] private string? updateText;
[ObservableProperty] private int progressValue;
[ObservableProperty] private bool showProgressBar;
public UpdateViewModel(
ISettingsManager settingsManager,
IHttpClientFactory httpClientFactory,
IUpdateHelper updateHelper)
{
this.settingsManager = settingsManager;
this.httpClientFactory = httpClientFactory;
this.updateHelper = updateHelper;
EventManager.Instance.UpdateAvailable += (_, info) =>
{
IsUpdateAvailable = true;
UpdateInfo = info;
};
updateHelper.StartCheckingForUpdates().SafeFireAndForget();
}
public override async Task OnLoadedAsync()
{
UpdateText = $"Stability Matrix v{UpdateInfo?.Version} is now available! You currently have v{Utilities.GetAppVersion()}. Would you like to update now?";
var client = httpClientFactory.CreateClient();
var response = await client.GetAsync(UpdateInfo?.ChangelogUrl);
if (response.IsSuccessStatusCode)
{
ReleaseNotes = await response.Content.ReadAsStringAsync();
}
else
{
ReleaseNotes = "## Unable to load release notes";
}
}
[RelayCommand]
private async Task InstallUpdate()
{
if (UpdateInfo == null)
{
return;
}
ShowProgressBar = true;
UpdateText = $"Downloading update v{UpdateInfo.Version}...";
await updateHelper.DownloadUpdate(UpdateInfo, new Progress<ProgressReport>(report =>
{
ProgressValue = Convert.ToInt32(report.Percentage);
}));
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";
await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds...";
await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 1 second...";
await Task.Delay(1000);
Process.Start(UpdateHelper.ExecutablePath);
App.Shutdown();
}
}

22
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.Primitives;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
@ -37,6 +38,7 @@ public partial class MainWindowViewModel : ViewModelBase
private List<PageViewModelBase> footerPages = new(); private List<PageViewModelBase> footerPages = new();
public ProgressManagerViewModel ProgressManagerViewModel { get; init; } public ProgressManagerViewModel ProgressManagerViewModel { get; init; }
public UpdateViewModel UpdateViewModel { get; init; }
public MainWindowViewModel(ISettingsManager settingsManager, ServiceManager<ViewModelBase> dialogFactory) public MainWindowViewModel(ISettingsManager settingsManager, ServiceManager<ViewModelBase> dialogFactory)
{ {
@ -44,6 +46,7 @@ public partial class MainWindowViewModel : ViewModelBase
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>(); ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>();
UpdateViewModel = dialogFactory.Get<UpdateViewModel>();
} }
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
@ -133,6 +136,25 @@ public partial class MainWindowViewModel : ViewModelBase
} }
} }
public async Task ShowUpdateDialog()
{
var viewModel = dialogFactory.Get<UpdateViewModel>();
var dialog = new BetterContentDialog
{
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new UpdateDialog
{
DataContext = viewModel
}
};
await dialog.ShowAsync();
}
private void OnPageChangeRequested(object? sender, Type e) private void OnPageChangeRequested(object? sender, Type e)
{ {
CurrentPage = Pages.FirstOrDefault(p => p.GetType() == e); CurrentPage = Pages.FirstOrDefault(p => p.GetType() == e);

85
StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml

@ -0,0 +1,85 @@
<controls:UserControlBase 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:StabilityMatrix.Avalonia.ViewModels"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
d:DataContext="{x:Static mocks:DesignData.UpdateViewModel}"
x:DataType="dialogs:UpdateViewModel"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="550"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.UpdateDialog">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto">
<!--<TitleBar Background="{ui:ThemeResource ApplicationBackgroundBrush}">
<TitleBar.Header>
<TextBlock Margin="16,8" Text="Stability Matrix - Update Available" />
</TitleBar.Header>
</TitleBar>-->
<TextBlock Grid.Row="0"
Text="A new version of Stability Matrix is available!"
HorizontalAlignment="Center"
FontWeight="Thin"
Margin="0,16,0,0"
FontSize="28"/>
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
FontSize="18"
TextWrapping="Wrap"
TextAlignment="Center"
Text="{Binding UpdateText}"
Margin="16,32,16,0"/>
<TextBlock Grid.Row="2"
Text="Release Notes"
FontSize="16"
IsVisible="{Binding !ShowProgressBar}"
Margin="32,16,32,0"/>
<ProgressBar Grid.Row="3"
Height="200"
Value="{Binding ProgressValue}"
IsVisible="{Binding ShowProgressBar}"
Margin="32"/>
<Grid Grid.Row="4"
Margin="8"
IsVisible="{Binding !ShowProgressBar}">
<Border Margin="32, 16"
CornerRadius="8"
Background="{DynamicResource ButtonBackgroundPressed}">
<mdxaml:MarkdownScrollViewer
Margin="16"
Markdown="{Binding ReleaseNotes, Mode=OneWay}"/>
</Border>
</Grid>
<ui:InfoBar Grid.Row="5"
Margin="64,0,64,16"
IsOpen="True"
IsClosable="False"
Title="The app will relaunch after updating" />
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
<Button Content="Remind Me Later"
Classes="info"
Margin="0,0,8,0"
FontSize="18"
Command="{Binding OnCloseButtonClick}"
IsEnabled="{Binding !InstallUpdateCommand.IsRunning}" />
<Button Content="Install Now"
Classes="success"
Margin="8,0,0,0"
FontSize="18"
Command="{Binding InstallUpdateCommand}" />
</StackPanel>
</Grid>
</controls:UserControlBase>

18
StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml.cs

@ -0,0 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
public partial class UpdateDialog : UserControl
{
public UpdateDialog()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

3
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -79,6 +79,9 @@
<!-- Update button --> <!-- Update button -->
<ui:NavigationViewItem Name="FooterUpdateItem" <ui:NavigationViewItem Name="FooterUpdateItem"
Content="Update Available" Content="Update Available"
IsVisible="{Binding IsUpdateAvailable}"
IsEnabled="{Binding IsUpdateAvailable}"
DataContext="{Binding UpdateViewModel}"
Tapped="FooterUpdateItem_OnTapped"> Tapped="FooterUpdateItem_OnTapped">
<ui:NavigationViewItem.IconSource> <ui:NavigationViewItem.IconSource>
<ui:SymbolIconSource Symbol="Download" Foreground="LimeGreen"/> <ui:SymbolIconSource Symbol="Download" Foreground="LimeGreen"/>

8
StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using AsyncAwaitBestPractices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
@ -7,12 +8,14 @@ using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Styling; using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media; using FluentAvalonia.UI.Media;
using FluentAvalonia.UI.Windowing; using FluentAvalonia.UI.Windowing;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.Views; namespace StabilityMatrix.Avalonia.Views;
@ -106,6 +109,11 @@ public partial class MainWindow : AppWindowBase
private void FooterUpdateItem_OnTapped(object? sender, TappedEventArgs e) private void FooterUpdateItem_OnTapped(object? sender, TappedEventArgs e)
{ {
// show update window thing // show update window thing
if (DataContext is not MainWindowViewModel vm)
{
throw new NullReferenceException("DataContext is not MainWindowViewModel");
}
Dispatcher.UIThread.InvokeAsync(vm.ShowUpdateDialog).SafeFireAndForget();
} }
private void FooterDiscordItem_OnTapped(object? sender, TappedEventArgs e) private void FooterDiscordItem_OnTapped(object? sender, TappedEventArgs e)

Loading…
Cancel
Save