Ionite
1 year ago
committed by
GitHub
74 changed files with 2227 additions and 282 deletions
@ -0,0 +1,24 @@ |
|||||||
|
using System; |
||||||
|
using System.Globalization; |
||||||
|
using System.Windows.Data; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Converters; |
||||||
|
|
||||||
|
[ValueConversion(typeof(string), typeof(bool))] |
||||||
|
public class IsStringNullOrWhitespaceConverter : IValueConverter |
||||||
|
{ |
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
if (value is string strValue) |
||||||
|
{ |
||||||
|
return string.IsNullOrWhiteSpace(strValue); |
||||||
|
} |
||||||
|
|
||||||
|
throw new InvalidOperationException("Cannot convert non-string value"); |
||||||
|
} |
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
throw new NotImplementedException(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
<ui:ContentDialog |
||||||
|
CloseButtonText="Close" |
||||||
|
Loaded="DataDirectoryMigrationDialog_OnLoaded" |
||||||
|
Title="Package Information Update" |
||||||
|
d:DataContext="{d:DesignInstance Type=viewModels:DataDirectoryMigrationViewModel, |
||||||
|
IsDesignTimeCreatable=True}" |
||||||
|
d:DesignHeight="512" |
||||||
|
d:DesignWidth="640" |
||||||
|
mc:Ignorable="d" |
||||||
|
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}" |
||||||
|
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}" |
||||||
|
x:Class="StabilityMatrix.DataDirectoryMigrationDialog" |
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||||
|
xmlns:converters="clr-namespace:StabilityMatrix.Converters" |
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||||
|
xmlns:local="clr-namespace:StabilityMatrix" |
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||||
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" |
||||||
|
xmlns:viewModels="clr-namespace:StabilityMatrix.ViewModels" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||||
|
|
||||||
|
<ui:ContentDialog.Resources> |
||||||
|
<!-- ReSharper disable once Xaml.StaticResourceNotResolved --> |
||||||
|
<Style BasedOn="{StaticResource {x:Type ui:ContentDialog}}" TargetType="{x:Type local:DataDirectoryMigrationDialog}" /> |
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /> |
||||||
|
<converters:ValueConverterGroup x:Key="InvertIsStringNullOrWhitespaceConverter"> |
||||||
|
<converters:IsStringNullOrWhitespaceConverter /> |
||||||
|
<converters:BoolNegationConverter /> |
||||||
|
</converters:ValueConverterGroup> |
||||||
|
</ui:ContentDialog.Resources> |
||||||
|
|
||||||
|
<Grid |
||||||
|
Margin="16" |
||||||
|
MaxWidth="700" |
||||||
|
MinHeight="300" |
||||||
|
VerticalAlignment="Stretch"> |
||||||
|
<Grid.RowDefinitions> |
||||||
|
<RowDefinition Height="*" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
</Grid.RowDefinitions> |
||||||
|
|
||||||
|
<StackPanel> |
||||||
|
<TextBlock |
||||||
|
FontSize="16" |
||||||
|
Margin="8,8,8,8" |
||||||
|
Text="It looks like package information for this Data Directory was created in an older version of Stability Matrix before v1.1.0, and needs to be upgraded." |
||||||
|
TextWrapping="Wrap" /> |
||||||
|
<ui:InfoBar |
||||||
|
IsClosable="False" |
||||||
|
IsOpen="{Binding AutoMigrateText, Converter={StaticResource InvertIsStringNullOrWhitespaceConverter}}" |
||||||
|
Margin="8" |
||||||
|
Severity="Success" |
||||||
|
Title="{Binding AutoMigrateText, FallbackValue=5 Packages will be automatically migrated to the new format}" /> |
||||||
|
<ui:InfoBar |
||||||
|
IsClosable="False" |
||||||
|
IsOpen="{Binding NeedsMoveMigrateText, Converter={StaticResource InvertIsStringNullOrWhitespaceConverter}}" |
||||||
|
Margin="8" |
||||||
|
Severity="Informational" |
||||||
|
Title="{Binding NeedsMoveMigrateText, FallbackValue=2 Packages are not relative to the Data Directory and will be moved this may take a few minutes}" /> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<!-- Progress for moving --> |
||||||
|
<StackPanel |
||||||
|
Grid.Row="1" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
Margin="8" |
||||||
|
Orientation="Horizontal" |
||||||
|
Visibility="{Binding MigrateCommand.IsRunning, Converter={StaticResource BooleanToVisibilityConverter}}"> |
||||||
|
<ui:ProgressRing |
||||||
|
Height="32" |
||||||
|
IsEnabled="{Binding MigrateCommand.IsRunning, Converter={StaticResource BooleanToVisibilityConverter}}" |
||||||
|
IsIndeterminate="True" |
||||||
|
Width="32" /> |
||||||
|
<TextBlock |
||||||
|
FontSize="16" |
||||||
|
Margin="8" |
||||||
|
Text="{Binding MigrateProgressText, FallbackValue=Migrating 1 of 2 Packages}" |
||||||
|
VerticalAlignment="Center" /> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<ui:Button |
||||||
|
Appearance="Success" |
||||||
|
Click="ContinueButton_OnClick" |
||||||
|
Content="Continue" |
||||||
|
FontSize="16" |
||||||
|
Grid.Row="2" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
Margin="8" |
||||||
|
Padding="16,8" /> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
</ui:ContentDialog> |
@ -0,0 +1,27 @@ |
|||||||
|
using System.Windows; |
||||||
|
using StabilityMatrix.ViewModels; |
||||||
|
using Wpf.Ui.Contracts; |
||||||
|
using Wpf.Ui.Controls.ContentDialogControl; |
||||||
|
|
||||||
|
namespace StabilityMatrix; |
||||||
|
|
||||||
|
public partial class DataDirectoryMigrationDialog : ContentDialog |
||||||
|
{ |
||||||
|
public DataDirectoryMigrationDialog(IContentDialogService dialogService, DataDirectoryMigrationViewModel viewModel) : base( |
||||||
|
dialogService.GetContentPresenter()) |
||||||
|
{ |
||||||
|
InitializeComponent(); |
||||||
|
DataContext = viewModel; |
||||||
|
} |
||||||
|
|
||||||
|
private async void ContinueButton_OnClick(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
await ((DataDirectoryMigrationViewModel) DataContext).MigrateCommand.ExecuteAsync(null); |
||||||
|
Hide(ContentDialogResult.Primary); |
||||||
|
} |
||||||
|
|
||||||
|
private void DataDirectoryMigrationDialog_OnLoaded(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
((DataDirectoryMigrationViewModel) DataContext).OnLoaded(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
using System; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using StabilityMatrix.Models; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Helper; |
||||||
|
|
||||||
|
public interface IUpdateHelper |
||||||
|
{ |
||||||
|
Task StartCheckingForUpdates(); |
||||||
|
|
||||||
|
Task DownloadUpdate(UpdateInfo updateInfo, |
||||||
|
IProgress<ProgressReport> progress); |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Net.Http; |
||||||
|
using System.Text.Json; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using System.Windows.Threading; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
using StabilityMatrix.Models; |
||||||
|
using StabilityMatrix.Services; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Helper; |
||||||
|
|
||||||
|
public class UpdateHelper : IUpdateHelper |
||||||
|
{ |
||||||
|
private readonly ILogger<UpdateHelper> logger; |
||||||
|
private readonly IHttpClientFactory httpClientFactory; |
||||||
|
private readonly IDownloadService downloadService; |
||||||
|
private readonly DispatcherTimer timer = new(); |
||||||
|
|
||||||
|
private static readonly string UpdateFolder = |
||||||
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Update"); |
||||||
|
|
||||||
|
public static readonly string ExecutablePath = |
||||||
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Update", "StabilityMatrix.exe"); |
||||||
|
|
||||||
|
public UpdateHelper(ILogger<UpdateHelper> logger, IHttpClientFactory httpClientFactory, |
||||||
|
IDownloadService downloadService) |
||||||
|
{ |
||||||
|
this.logger = logger; |
||||||
|
this.httpClientFactory = httpClientFactory; |
||||||
|
this.downloadService = downloadService; |
||||||
|
|
||||||
|
timer.Interval = TimeSpan.FromMinutes(5); |
||||||
|
timer.Tick += async (_, _) => { await CheckForUpdate(); }; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task StartCheckingForUpdates() |
||||||
|
{ |
||||||
|
timer.IsEnabled = true; |
||||||
|
timer.Start(); |
||||||
|
await CheckForUpdate(); |
||||||
|
} |
||||||
|
|
||||||
|
public async Task DownloadUpdate(UpdateInfo updateInfo, |
||||||
|
IProgress<ProgressReport> progress) |
||||||
|
{ |
||||||
|
var downloadUrl = updateInfo.DownloadUrl; |
||||||
|
|
||||||
|
Directory.CreateDirectory(UpdateFolder); |
||||||
|
|
||||||
|
// download the file from URL |
||||||
|
await downloadService.DownloadToFileAsync(downloadUrl, ExecutablePath, progress: progress, |
||||||
|
httpClientName: "UpdateClient"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task CheckForUpdate() |
||||||
|
{ |
||||||
|
var httpClient = httpClientFactory.CreateClient("UpdateClient"); |
||||||
|
var response = await httpClient.GetAsync("https://cdn.lykos.ai/update.json"); |
||||||
|
if (!response.IsSuccessStatusCode) |
||||||
|
{ |
||||||
|
logger.LogError("Error while checking for update"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var updateInfo = |
||||||
|
await JsonSerializer.DeserializeAsync<UpdateInfo>( |
||||||
|
await response.Content.ReadAsStreamAsync()); |
||||||
|
|
||||||
|
if (updateInfo == null) |
||||||
|
{ |
||||||
|
logger.LogError("UpdateInfo is null"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (updateInfo.Version == Utilities.GetAppVersion()) |
||||||
|
{ |
||||||
|
logger.LogInformation("No update available"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// check if update is newer |
||||||
|
var updateVersion = updateInfo.Version.Split('.'); |
||||||
|
var currentVersion = Utilities.GetAppVersion().Split('.'); |
||||||
|
if (updateVersion.Length != 4 || currentVersion.Length != 4) |
||||||
|
{ |
||||||
|
logger.LogError("Invalid version format"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var updateVersionInt = new int[4]; |
||||||
|
var currentVersionInt = new int[4]; |
||||||
|
for (var i = 0; i < 4; i++) |
||||||
|
{ |
||||||
|
if (int.TryParse(updateVersion[i], out updateVersionInt[i]) && |
||||||
|
int.TryParse(currentVersion[i], out currentVersionInt[i])) continue; |
||||||
|
logger.LogError("Invalid version format"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// check if update is newer |
||||||
|
for (var i = 0; i < 4; i++) |
||||||
|
{ |
||||||
|
if (updateVersionInt[i] <= currentVersionInt[i]) continue; |
||||||
|
|
||||||
|
logger.LogInformation("Update available"); |
||||||
|
EventManager.Instance.OnUpdateAvailable(updateInfo); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
using System.Reflection; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Helper; |
||||||
|
|
||||||
|
public static class Utilities |
||||||
|
{ |
||||||
|
public static string GetAppVersion() |
||||||
|
{ |
||||||
|
var assembly = Assembly.GetExecutingAssembly(); |
||||||
|
var version = assembly.GetName().Version; |
||||||
|
return version == null |
||||||
|
? "(Unknown)" |
||||||
|
: $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models.FileInterfaces; |
||||||
|
|
||||||
|
public class DirectoryPath : FileSystemPath, IPathObject |
||||||
|
{ |
||||||
|
private DirectoryInfo? _info; |
||||||
|
// ReSharper disable once MemberCanBePrivate.Global |
||||||
|
public DirectoryInfo Info => _info ??= new DirectoryInfo(FullPath); |
||||||
|
|
||||||
|
public bool IsSymbolicLink |
||||||
|
{ |
||||||
|
get |
||||||
|
{ |
||||||
|
Info.Refresh(); |
||||||
|
return Info.Attributes.HasFlag(FileAttributes.ReparsePoint); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public bool Exists => Info.Exists; |
||||||
|
|
||||||
|
public DirectoryPath(string path) : base(path) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public DirectoryPath(FileSystemPath path) : base(path) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public DirectoryPath(params string[] paths) : base(paths) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public long GetSize() |
||||||
|
{ |
||||||
|
ulong size = 1 + 2; |
||||||
|
Info.Refresh(); |
||||||
|
return Info.EnumerateFiles("*", SearchOption.AllDirectories) |
||||||
|
.Sum(file => file.Length); |
||||||
|
} |
||||||
|
|
||||||
|
public long GetSize(bool includeSymbolicLinks) |
||||||
|
{ |
||||||
|
if (includeSymbolicLinks) return GetSize(); |
||||||
|
|
||||||
|
Info.Refresh(); |
||||||
|
var files = Info.GetFiles() |
||||||
|
.Where(file => !file.Attributes.HasFlag(FileAttributes.ReparsePoint)) |
||||||
|
.Sum(file => file.Length); |
||||||
|
var subDirs = Info.GetDirectories() |
||||||
|
.Where(dir => !dir.Attributes.HasFlag(FileAttributes.ReparsePoint)) |
||||||
|
.Sum(dir => dir.EnumerateFiles("*", SearchOption.AllDirectories).Sum(file => file.Length)); |
||||||
|
return files + subDirs; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<long> GetSizeAsync(bool includeSymbolicLinks) |
||||||
|
{ |
||||||
|
return Task.Run(() => GetSize(includeSymbolicLinks)); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> Creates the directory. </summary> |
||||||
|
public void Create() => Directory.CreateDirectory(FullPath); |
||||||
|
|
||||||
|
/// <summary> Deletes the directory. </summary> |
||||||
|
public void Delete() => Directory.Delete(FullPath); |
||||||
|
|
||||||
|
/// <summary> Deletes the directory asynchronously. </summary> |
||||||
|
public Task DeleteAsync() => Task.Run(Delete); |
||||||
|
|
||||||
|
/// <summary> Deletes the directory. </summary> |
||||||
|
public void Delete(bool recursive) => Directory.Delete(FullPath, recursive); |
||||||
|
|
||||||
|
/// <summary> Deletes the directory asynchronously. </summary> |
||||||
|
public Task DeleteAsync(bool recursive) => Task.Run(() => Delete(recursive)); |
||||||
|
|
||||||
|
public override string ToString() => FullPath; |
||||||
|
|
||||||
|
// DirectoryPath + DirectoryPath = DirectoryPath |
||||||
|
public static DirectoryPath operator +(DirectoryPath path, DirectoryPath other) => new(Path.Combine(path, other.FullPath)); |
||||||
|
|
||||||
|
// DirectoryPath + FilePath = FilePath |
||||||
|
public static FilePath operator +(DirectoryPath path, FilePath other) => new(Path.Combine(path, other.FullPath)); |
||||||
|
|
||||||
|
// DirectoryPath + string = string |
||||||
|
public static string operator +(DirectoryPath path, string other) => Path.Combine(path, other); |
||||||
|
|
||||||
|
// Implicit conversions to and from string |
||||||
|
public static implicit operator string(DirectoryPath path) => path.FullPath; |
||||||
|
public static implicit operator DirectoryPath(string path) => new(path); |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
using System.Diagnostics.CodeAnalysis; |
||||||
|
using System.IO; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models.FileInterfaces; |
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] |
||||||
|
public class FilePath : FileSystemPath, IPathObject |
||||||
|
{ |
||||||
|
private FileInfo? _info; |
||||||
|
// ReSharper disable once MemberCanBePrivate.Global |
||||||
|
public FileInfo Info => _info ??= new FileInfo(FullPath); |
||||||
|
|
||||||
|
public bool IsSymbolicLink |
||||||
|
{ |
||||||
|
get |
||||||
|
{ |
||||||
|
Info.Refresh(); |
||||||
|
return Info.Attributes.HasFlag(FileAttributes.ReparsePoint); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public bool Exists => Info.Exists; |
||||||
|
|
||||||
|
public FilePath(string path) : base(path) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public FilePath(FileSystemPath path) : base(path) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public FilePath(params string[] paths) : base(paths) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public long GetSize() |
||||||
|
{ |
||||||
|
Info.Refresh(); |
||||||
|
return Info.Length; |
||||||
|
} |
||||||
|
|
||||||
|
public long GetSize(bool includeSymbolicLinks) |
||||||
|
{ |
||||||
|
if (!includeSymbolicLinks && IsSymbolicLink) return 0; |
||||||
|
return GetSize(); |
||||||
|
} |
||||||
|
|
||||||
|
public Task<long> GetSizeAsync(bool includeSymbolicLinks) |
||||||
|
{ |
||||||
|
return Task.Run(() => GetSize(includeSymbolicLinks)); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> Creates an empty file. </summary> |
||||||
|
public void Create() => File.Create(FullPath).Close(); |
||||||
|
|
||||||
|
/// <summary> Deletes the file </summary> |
||||||
|
public void Delete() => File.Delete(FullPath); |
||||||
|
|
||||||
|
// Methods specific to files |
||||||
|
|
||||||
|
/// <summary> Read text </summary> |
||||||
|
public string ReadAllText() => File.ReadAllText(FullPath); |
||||||
|
|
||||||
|
/// <summary> Read text asynchronously </summary> |
||||||
|
public Task<string> ReadAllTextAsync(CancellationToken ct = default) |
||||||
|
{ |
||||||
|
return File.ReadAllTextAsync(FullPath, ct); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> Read bytes </summary> |
||||||
|
public byte[] ReadAllBytes() => File.ReadAllBytes(FullPath); |
||||||
|
|
||||||
|
/// <summary> Read bytes asynchronously </summary> |
||||||
|
public Task<byte[]> ReadAllBytesAsync(CancellationToken ct = default) |
||||||
|
{ |
||||||
|
return File.ReadAllBytesAsync(FullPath, ct); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> Write bytes </summary> |
||||||
|
public void WriteAllBytes(byte[] bytes) => File.WriteAllBytes(FullPath, bytes); |
||||||
|
|
||||||
|
/// <summary> Write bytes asynchronously </summary> |
||||||
|
public Task WriteAllBytesAsync(byte[] bytes, CancellationToken ct = default) |
||||||
|
{ |
||||||
|
return File.WriteAllBytesAsync(FullPath, bytes, ct); |
||||||
|
} |
||||||
|
|
||||||
|
// Implicit conversions to and from string |
||||||
|
public static implicit operator string(FilePath path) => path.FullPath; |
||||||
|
public static implicit operator FilePath(string path) => new(path); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
using System.IO; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models.FileInterfaces; |
||||||
|
|
||||||
|
public class FileSystemPath |
||||||
|
{ |
||||||
|
public string FullPath { get; } |
||||||
|
|
||||||
|
protected FileSystemPath(string path) |
||||||
|
{ |
||||||
|
FullPath = path; |
||||||
|
} |
||||||
|
|
||||||
|
protected FileSystemPath(FileSystemPath path) : this(path.FullPath) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
protected FileSystemPath(params string[] paths) : this(Path.Combine(paths)) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models.FileInterfaces; |
||||||
|
|
||||||
|
public interface IPathObject |
||||||
|
{ |
||||||
|
/// <summary> Full path of the file system object. </summary> |
||||||
|
string FullPath { get; } |
||||||
|
|
||||||
|
/// <summary> Whether the file system object is a symbolic link or junction. </summary> |
||||||
|
bool IsSymbolicLink { get; } |
||||||
|
|
||||||
|
/// <summary> Gets the size of the file system object. </summary> |
||||||
|
long GetSize(); |
||||||
|
|
||||||
|
/// <summary> Gets the size of the file system object asynchronously. </summary> |
||||||
|
Task<long> GetSizeAsync() => Task.Run(GetSize); |
||||||
|
|
||||||
|
/// <summary> Whether the file system object exists. </summary> |
||||||
|
bool Exists { get; } |
||||||
|
|
||||||
|
/// <summary> Deletes the file system object </summary> |
||||||
|
void Delete(); |
||||||
|
|
||||||
|
/// <summary> Deletes the file system object asynchronously. </summary> |
||||||
|
public Task DeleteAsync() => Task.Run(Delete); |
||||||
|
} |
@ -0,0 +1,146 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.DirectoryServices; |
||||||
|
using System.Linq; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using System.Security; |
||||||
|
using System.Security.Cryptography; |
||||||
|
using System.Security.Principal; |
||||||
|
using System.Text.Json; |
||||||
|
using System.Text.Json.Serialization; |
||||||
|
using StabilityMatrix.Models.FileInterfaces; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models; |
||||||
|
|
||||||
|
internal record struct KeyInfo(byte[] Key, byte[] Salt, int Iterations); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Global instance of user secrets. |
||||||
|
/// Stored in %APPDATA%\StabilityMatrix\user-secrets.data |
||||||
|
/// </summary> |
||||||
|
public class GlobalUserSecrets |
||||||
|
{ |
||||||
|
private const int KeySize = 32; |
||||||
|
private const int Iterations = 300; |
||||||
|
private const int SaltSize = 16; |
||||||
|
|
||||||
|
[JsonIgnore] |
||||||
|
public static FilePath File { get; } = GlobalConfig.HomeDir + "user-secrets.data"; |
||||||
|
|
||||||
|
public Dictionary<string, string> PatreonCookies { get; set; } = new(); |
||||||
|
|
||||||
|
private static SecurityIdentifier? GetComputerSid() |
||||||
|
{ |
||||||
|
var id = new DirectoryEntry($"WinNT://{Environment.MachineName},Computer") |
||||||
|
.Children.Cast<DirectoryEntry>().First().InvokeGet("objectSID"); |
||||||
|
return id is not byte[] bytes ? null : |
||||||
|
new SecurityIdentifier(bytes, 0).AccountDomainSid; |
||||||
|
} |
||||||
|
|
||||||
|
private static SecureString GetComputerKeyPhrase() |
||||||
|
{ |
||||||
|
var keySource = GetComputerSid()?.ToString(); |
||||||
|
// If no sid, use username as fallback |
||||||
|
keySource ??= Environment.UserName; |
||||||
|
|
||||||
|
// XOR with fixed constant |
||||||
|
const string keyPhrase = "StabilityMatrix"; |
||||||
|
var result = new SecureString(); |
||||||
|
|
||||||
|
for (var i = 0; i < keySource.Length; i++) |
||||||
|
{ |
||||||
|
result.AppendChar((char)(keySource[i] ^ keyPhrase[i % keyPhrase.Length])); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private static KeyInfo DeriveKeyWithSalt(SecureString password, int saltLength, int iterations, int keyLength) |
||||||
|
{ |
||||||
|
var salt = RandomNumberGenerator.GetBytes(saltLength); |
||||||
|
var key = DeriveKey(password, salt, iterations, keyLength); |
||||||
|
return new KeyInfo(key, salt, iterations); |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] DeriveKey(SecureString password, byte[] salt, int iterations, int keyLength) |
||||||
|
{ |
||||||
|
var ptr = Marshal.SecureStringToBSTR(password); |
||||||
|
try |
||||||
|
{ |
||||||
|
var length = Marshal.ReadInt32(ptr, -4); |
||||||
|
var passwordByteArray = new byte[length]; |
||||||
|
var handle = GCHandle.Alloc(passwordByteArray, GCHandleType.Pinned); |
||||||
|
try |
||||||
|
{ |
||||||
|
for (var i = 0; i < length; i++) |
||||||
|
{ |
||||||
|
passwordByteArray[i] = Marshal.ReadByte(ptr, i); |
||||||
|
} |
||||||
|
|
||||||
|
using var rfc2898 = new Rfc2898DeriveBytes(passwordByteArray, salt, iterations); |
||||||
|
return rfc2898.GetBytes(keyLength); |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
Array.Clear(passwordByteArray, 0, passwordByteArray.Length); |
||||||
|
handle.Free(); |
||||||
|
} |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
Marshal.ZeroFreeBSTR(ptr); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static (byte[], byte[]) EncryptBytes(byte[] data) |
||||||
|
{ |
||||||
|
var keyInfo = |
||||||
|
DeriveKeyWithSalt(GetComputerKeyPhrase(), SaltSize, Iterations, KeySize); |
||||||
|
|
||||||
|
using var aes = Aes.Create(); |
||||||
|
aes.Key = keyInfo.Key; |
||||||
|
aes.IV = keyInfo.Salt; |
||||||
|
aes.Padding = PaddingMode.PKCS7; |
||||||
|
aes.Mode = CipherMode.CBC; |
||||||
|
|
||||||
|
var transform = aes.CreateEncryptor(); |
||||||
|
return (transform.TransformFinalBlock(data, 0, data.Length), keyInfo.Salt); |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] DecryptBytes(IReadOnlyCollection<byte> encryptedData, byte[] salt) |
||||||
|
{ |
||||||
|
var key = |
||||||
|
DeriveKey(GetComputerKeyPhrase(), salt, Iterations, KeySize); |
||||||
|
|
||||||
|
using var aes = Aes.Create(); |
||||||
|
aes.Key = key; |
||||||
|
aes.IV = salt; |
||||||
|
aes.Padding = PaddingMode.PKCS7; |
||||||
|
aes.Mode = CipherMode.CBC; |
||||||
|
|
||||||
|
var transform = aes.CreateDecryptor(); |
||||||
|
return transform.TransformFinalBlock(encryptedData.ToArray(), 0, encryptedData.Count); |
||||||
|
} |
||||||
|
|
||||||
|
public void SaveToFile() |
||||||
|
{ |
||||||
|
var json = JsonSerializer.SerializeToUtf8Bytes(this); |
||||||
|
var (encrypted, salt) = EncryptBytes(json); |
||||||
|
// Prepend salt to encrypted json |
||||||
|
var fileBytes = salt.Concat(encrypted).ToArray(); |
||||||
|
File.WriteAllBytes(fileBytes); |
||||||
|
} |
||||||
|
|
||||||
|
public static GlobalUserSecrets? LoadFromFile() |
||||||
|
{ |
||||||
|
var fileBytes = File.ReadAllBytes(); |
||||||
|
|
||||||
|
// Get salt from start of file |
||||||
|
var salt = fileBytes.AsSpan(0, SaltSize).ToArray(); |
||||||
|
// Get encrypted json from rest of file |
||||||
|
var encryptedJson = fileBytes.AsSpan(SaltSize).ToArray(); |
||||||
|
|
||||||
|
var json = DecryptBytes(encryptedJson, salt); |
||||||
|
return JsonSerializer.Deserialize<GlobalUserSecrets>(json); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
namespace StabilityMatrix.Models.Settings; |
||||||
|
|
||||||
|
public class GlobalSettings |
||||||
|
{ |
||||||
|
public bool EulaAccepted { get; set; } |
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
namespace StabilityMatrix.Models; |
namespace StabilityMatrix.Models.Settings; |
||||||
|
|
||||||
public class LibrarySettings |
public class LibrarySettings |
||||||
{ |
{ |
@ -0,0 +1,7 @@ |
|||||||
|
namespace StabilityMatrix.Models.Settings; |
||||||
|
|
||||||
|
public class WindowSettings |
||||||
|
{ |
||||||
|
public double Width { get; set; } |
||||||
|
public double Height { get; set; } |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
using System.Text.Json.Serialization; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models; |
||||||
|
|
||||||
|
public class UpdateInfo |
||||||
|
{ |
||||||
|
[JsonPropertyName("version")] |
||||||
|
public string Version { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName("url")] |
||||||
|
public string DownloadUrl { get; set; } |
||||||
|
|
||||||
|
[JsonPropertyName("changelog")] |
||||||
|
public string ChangelogUrl { get; set; } |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
<ui:ContentDialog |
||||||
|
CloseButtonText="Close" |
||||||
|
Loaded="SelectInstallLocationsDialog_OnLoaded" |
||||||
|
Title="Select Data Directory" |
||||||
|
d:DataContext="{d:DesignInstance Type=viewModels:SelectInstallLocationsViewModel, |
||||||
|
IsDesignTimeCreatable=True}" |
||||||
|
d:DesignHeight="512" |
||||||
|
d:DesignWidth="640" |
||||||
|
mc:Ignorable="d" |
||||||
|
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}" |
||||||
|
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}" |
||||||
|
x:Class="StabilityMatrix.SelectInstallLocationsDialog" |
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||||
|
xmlns:controls="clr-namespace:StabilityMatrix.Controls" |
||||||
|
xmlns:converters="clr-namespace:StabilityMatrix.Converters" |
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||||
|
xmlns:local="clr-namespace:StabilityMatrix" |
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||||
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" |
||||||
|
xmlns:viewModels="clr-namespace:StabilityMatrix.ViewModels" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||||
|
|
||||||
|
<ui:ContentDialog.Resources> |
||||||
|
<!-- ReSharper disable once Xaml.StaticResourceNotResolved --> |
||||||
|
<Style BasedOn="{StaticResource {x:Type ui:ContentDialog}}" TargetType="{x:Type local:SelectInstallLocationsDialog}" /> |
||||||
|
<converters:BoolNegationConverter x:Key="BoolNegationConverter" /> |
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /> |
||||||
|
</ui:ContentDialog.Resources> |
||||||
|
|
||||||
|
<Grid |
||||||
|
Margin="16,0,16,16" |
||||||
|
MaxWidth="700" |
||||||
|
MinHeight="400"> |
||||||
|
<Grid.RowDefinitions> |
||||||
|
<RowDefinition Height="*" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
</Grid.RowDefinitions> |
||||||
|
|
||||||
|
<StackPanel Grid.Row="0" Margin="0,0,0,16"> |
||||||
|
<Label |
||||||
|
Content="Data Directory" |
||||||
|
FontSize="13" |
||||||
|
Margin="0,16,0,0" /> |
||||||
|
<Grid> |
||||||
|
<Grid.ColumnDefinitions> |
||||||
|
<ColumnDefinition Width="*" /> |
||||||
|
<ColumnDefinition Width="Auto" /> |
||||||
|
</Grid.ColumnDefinitions> |
||||||
|
|
||||||
|
<ui:TextBox |
||||||
|
Height="36" |
||||||
|
IsEnabled="{Binding IsPortableMode, Converter={StaticResource BoolNegationConverter}}" |
||||||
|
Margin="0,0,8,0" |
||||||
|
PlaceholderEnabled="True" |
||||||
|
PlaceholderText="{Binding DefaultInstallLocation}" |
||||||
|
Text="{Binding DataDirectory, UpdateSourceTrigger=PropertyChanged}" |
||||||
|
VerticalAlignment="Stretch" /> |
||||||
|
|
||||||
|
<ui:Button |
||||||
|
Command="{Binding ShowFolderBrowserDialogCommand}" |
||||||
|
Grid.Column="1" |
||||||
|
Height="36" |
||||||
|
HorizontalAlignment="Stretch" |
||||||
|
IsEnabled="{Binding IsPortableMode, Converter={StaticResource BoolNegationConverter}}"> |
||||||
|
<ui:Button.Icon> |
||||||
|
<ui:SymbolIcon Symbol="FolderOpen24" /> |
||||||
|
</ui:Button.Icon> |
||||||
|
</ui:Button> |
||||||
|
</Grid> |
||||||
|
<Label |
||||||
|
Content="This is where the model checkpoints, LORAs, web UIs, settings, etc. will be installed." |
||||||
|
FontSize="12" |
||||||
|
Margin="0,8,0,0" /> |
||||||
|
|
||||||
|
<CheckBox |
||||||
|
Content="Portable Mode" |
||||||
|
IsChecked="{Binding IsPortableMode, Mode=TwoWay}" |
||||||
|
Margin="0,16,0,0" /> |
||||||
|
|
||||||
|
<ui:InfoBar |
||||||
|
IsClosable="False" |
||||||
|
IsOpen="True" |
||||||
|
Margin="0,8,0,0" |
||||||
|
Padding="8,16" |
||||||
|
Title="In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its 'Data' folder to a different location or computer." /> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<!-- Indicator of existing or new data directory --> |
||||||
|
<StackPanel |
||||||
|
Grid.Row="1" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
Margin="8,0,8,8" |
||||||
|
Orientation="Horizontal" |
||||||
|
Visibility="{Binding IsStatusBadgeVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> |
||||||
|
<controls:RefreshBadge DataContext="{Binding RefreshBadgeViewModel}" /> |
||||||
|
<TextBlock |
||||||
|
FontSize="14" |
||||||
|
Text="{Binding RefreshBadgeViewModel.CurrentToolTip}" |
||||||
|
VerticalAlignment="Center" /> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
<ui:Button |
||||||
|
Appearance="Success" |
||||||
|
Click="ContinueButton_OnClick" |
||||||
|
Content="Continue" |
||||||
|
FontSize="16" |
||||||
|
Grid.Row="2" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
IsEnabled="{Binding IsDirectoryValid}" |
||||||
|
Margin="8" |
||||||
|
Padding="16,8" /> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
</ui:ContentDialog> |
@ -0,0 +1,26 @@ |
|||||||
|
using System.Windows; |
||||||
|
using StabilityMatrix.ViewModels; |
||||||
|
using Wpf.Ui.Contracts; |
||||||
|
using Wpf.Ui.Controls.ContentDialogControl; |
||||||
|
|
||||||
|
namespace StabilityMatrix; |
||||||
|
|
||||||
|
public partial class SelectInstallLocationsDialog : ContentDialog |
||||||
|
{ |
||||||
|
public SelectInstallLocationsDialog(IContentDialogService dialogService, SelectInstallLocationsViewModel viewModel) : base( |
||||||
|
dialogService.GetContentPresenter()) |
||||||
|
{ |
||||||
|
InitializeComponent(); |
||||||
|
DataContext = viewModel; |
||||||
|
} |
||||||
|
|
||||||
|
private void ContinueButton_OnClick(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
Hide(ContentDialogResult.Primary); |
||||||
|
} |
||||||
|
|
||||||
|
private void SelectInstallLocationsDialog_OnLoaded(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
((SelectInstallLocationsViewModel) DataContext).OnLoaded(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
<ui:FluentWindow |
||||||
|
ExtendsContentIntoTitleBar="True" |
||||||
|
Height="700" |
||||||
|
Icon="pack://application:,,,/Assets/Icon.ico" |
||||||
|
Loaded="UpdateWindow_OnLoaded" |
||||||
|
Title="Stability Matrix - Update" |
||||||
|
Width="700" |
||||||
|
WindowBackdropType="{Binding WindowBackdropType}" |
||||||
|
WindowStartupLocation="CenterOwner" |
||||||
|
d:DataContext="{d:DesignInstance Type=viewModels:UpdateWindowViewModel, |
||||||
|
IsDesignTimeCreatable=True}" |
||||||
|
d:DesignHeight="700" |
||||||
|
d:DesignWidth="700" |
||||||
|
mc:Ignorable="d" |
||||||
|
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}" |
||||||
|
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}" |
||||||
|
x:Class="StabilityMatrix.UpdateWindow" |
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||||
|
xmlns:converters="clr-namespace:StabilityMatrix.Converters" |
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||||
|
xmlns:system="clr-namespace:System;assembly=System.Runtime" |
||||||
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" |
||||||
|
xmlns:viewModels="clr-namespace:StabilityMatrix.ViewModels" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:xaml="clr-namespace:MdXaml;assembly=MdXaml"> |
||||||
|
|
||||||
|
<ui:FluentWindow.Resources> |
||||||
|
<converters:ValueConverterGroup x:Key="InvertAndVisibilitate"> |
||||||
|
<converters:BoolNegationConverter /> |
||||||
|
<BooleanToVisibilityConverter /> |
||||||
|
</converters:ValueConverterGroup> |
||||||
|
|
||||||
|
<converters:BoolNegationConverter x:Key="BoolNegationConverter" /> |
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" /> |
||||||
|
<converters:UriToBitmapConverter x:Key="UriToBitmapConverter" /> |
||||||
|
|
||||||
|
<xaml:Markdown |
||||||
|
AssetPathRoot="{x:Static system:Environment.CurrentDirectory}" |
||||||
|
DocumentStyle="{StaticResource DocumentStyle}" |
||||||
|
Heading1Style="{StaticResource H1Style}" |
||||||
|
Heading2Style="{StaticResource H2Style}" |
||||||
|
Heading3Style="{StaticResource H3Style}" |
||||||
|
Heading4Style="{StaticResource H4Style}" |
||||||
|
ImageStyle="{StaticResource ImageStyle}" |
||||||
|
LinkStyle="{StaticResource LinkStyle}" |
||||||
|
SeparatorStyle="{StaticResource SeparatorStyle}" |
||||||
|
x:Key="Markdown" /> |
||||||
|
<xaml:TextToFlowDocumentConverter Markdown="{StaticResource Markdown}" x:Key="TextToFlowDocumentConverter" /> |
||||||
|
</ui:FluentWindow.Resources> |
||||||
|
|
||||||
|
<Grid> |
||||||
|
<Grid.RowDefinitions> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
<RowDefinition Height="*" /> |
||||||
|
<RowDefinition Height="Auto" /> |
||||||
|
</Grid.RowDefinitions> |
||||||
|
|
||||||
|
<ui:TitleBar Background="{ui:ThemeResource ApplicationBackgroundBrush}"> |
||||||
|
<ui:TitleBar.Header> |
||||||
|
<TextBlock Margin="16,8" Text="Stability Matrix - Update Available" /> |
||||||
|
</ui:TitleBar.Header> |
||||||
|
</ui:TitleBar> |
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" |
||||||
|
Text="A new version of Stability Matrix is available!" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
FontWeight="Thin" |
||||||
|
Margin="0,16,0,0" |
||||||
|
FontSize="28"/> |
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
FontSize="18" |
||||||
|
TextWrapping="Wrap" |
||||||
|
TextAlignment="Center" |
||||||
|
Text="{Binding UpdateText, FallbackValue=Update available and stuff}" |
||||||
|
Margin="16,32,16,0"/> |
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" |
||||||
|
Text="Release Notes" |
||||||
|
FontSize="16" |
||||||
|
Visibility="{Binding ShowProgressBar, Converter={StaticResource InvertAndVisibilitate}}" |
||||||
|
Margin="32,16,32,0"/> |
||||||
|
|
||||||
|
<ProgressBar Grid.Row="4" |
||||||
|
Height="200" |
||||||
|
Value="{Binding ProgressValue}" |
||||||
|
Visibility="{Binding ShowProgressBar, Converter={StaticResource BoolToVisConverter}}" |
||||||
|
Margin="32"/> |
||||||
|
|
||||||
|
<Grid Grid.Row="4" |
||||||
|
Visibility="{Binding ShowProgressBar, Converter={StaticResource InvertAndVisibilitate}}"> |
||||||
|
<Border Margin="32, 16" |
||||||
|
CornerRadius="16" |
||||||
|
Background="#66000000"/> |
||||||
|
|
||||||
|
<FlowDocumentScrollViewer |
||||||
|
Margin="32,16" |
||||||
|
Document="{Binding ReleaseNotes, Converter={StaticResource TextToFlowDocumentConverter}}" |
||||||
|
HorizontalAlignment="Stretch" |
||||||
|
VerticalAlignment="Stretch" /> |
||||||
|
</Grid> |
||||||
|
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16"> |
||||||
|
<ui:Button Content="Remind Me Later" |
||||||
|
Margin="0,0,8,0" |
||||||
|
FontSize="18" |
||||||
|
Click="RemindMeLaterButton_OnClick" |
||||||
|
Appearance="Info"/> |
||||||
|
|
||||||
|
<ui:Button Content="Install Now" |
||||||
|
Margin="8,0,0,0" |
||||||
|
FontSize="18" |
||||||
|
Command="{Binding InstallUpdateCommand}" |
||||||
|
Appearance="Success"/> |
||||||
|
</StackPanel> |
||||||
|
|
||||||
|
</Grid> |
||||||
|
</ui:FluentWindow> |
@ -0,0 +1,27 @@ |
|||||||
|
using System.Windows; |
||||||
|
using StabilityMatrix.ViewModels; |
||||||
|
using Wpf.Ui.Controls.Window; |
||||||
|
|
||||||
|
namespace StabilityMatrix; |
||||||
|
|
||||||
|
public partial class UpdateWindow : FluentWindow |
||||||
|
{ |
||||||
|
private readonly UpdateWindowViewModel viewModel; |
||||||
|
|
||||||
|
public UpdateWindow(UpdateWindowViewModel viewModel) |
||||||
|
{ |
||||||
|
this.viewModel = viewModel; |
||||||
|
InitializeComponent(); |
||||||
|
DataContext = viewModel; |
||||||
|
} |
||||||
|
|
||||||
|
private async void UpdateWindow_OnLoaded(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
await viewModel.OnLoaded(); |
||||||
|
} |
||||||
|
|
||||||
|
private void RemindMeLaterButton_OnClick(object sender, RoutedEventArgs e) |
||||||
|
{ |
||||||
|
Close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using CommunityToolkit.Mvvm.Input; |
||||||
|
using NLog; |
||||||
|
using StabilityMatrix.Helper; |
||||||
|
using StabilityMatrix.Python; |
||||||
|
|
||||||
|
namespace StabilityMatrix.ViewModels; |
||||||
|
|
||||||
|
public partial class DataDirectoryMigrationViewModel : ObservableObject |
||||||
|
{ |
||||||
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||||
|
private readonly ISettingsManager settingsManager; |
||||||
|
private readonly IPrerequisiteHelper prerequisiteHelper; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[NotifyPropertyChangedFor(nameof(AutoMigrateText))] |
||||||
|
[NotifyPropertyChangedFor(nameof(MigrateProgressText))] |
||||||
|
private int autoMigrateCount; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[NotifyPropertyChangedFor(nameof(NeedsMoveMigrateText))] |
||||||
|
[NotifyPropertyChangedFor(nameof(MigrateProgressText))] |
||||||
|
private int needsMoveMigrateCount; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[NotifyPropertyChangedFor(nameof(MigrateProgressText))] |
||||||
|
private int migrateProgressCount; |
||||||
|
|
||||||
|
public string AutoMigrateText => AutoMigrateCount == 0 ? string.Empty : |
||||||
|
$"{AutoMigrateCount} Packages will be automatically migrated to the new format"; |
||||||
|
|
||||||
|
public string NeedsMoveMigrateText => NeedsMoveMigrateCount == 0 ? string.Empty : |
||||||
|
$"{NeedsMoveMigrateCount} Packages are not relative to the Data Directory and will be moved, this may take a few minutes"; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private string migrateProgressText = ""; |
||||||
|
|
||||||
|
partial void OnMigrateProgressCountChanged(int value) |
||||||
|
{ |
||||||
|
MigrateProgressText = value > 0 ? $"Migrating {value} of {AutoMigrateCount + NeedsMoveMigrateCount} Packages" : string.Empty; |
||||||
|
} |
||||||
|
|
||||||
|
public DataDirectoryMigrationViewModel(ISettingsManager settingsManager, IPrerequisiteHelper prerequisiteHelper) |
||||||
|
{ |
||||||
|
this.settingsManager = settingsManager; |
||||||
|
this.prerequisiteHelper = prerequisiteHelper; |
||||||
|
} |
||||||
|
|
||||||
|
public void OnLoaded() |
||||||
|
{ |
||||||
|
AutoMigrateCount = 0; |
||||||
|
NeedsMoveMigrateCount = 0; |
||||||
|
|
||||||
|
// Get all old packages |
||||||
|
var oldPackages = settingsManager.GetOldInstalledPackages().ToArray(); |
||||||
|
// Attempt to migrate with pure, and count successful migrations |
||||||
|
AutoMigrateCount = oldPackages.Count(p => p.CanPureMigratePath()); |
||||||
|
// Any remaining packages need to be moved |
||||||
|
NeedsMoveMigrateCount = oldPackages.Length - AutoMigrateCount; |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task MigrateAsync() |
||||||
|
{ |
||||||
|
await using var delay = new MinimumDelay(200, 300); |
||||||
|
|
||||||
|
// Since we are going to recreate venvs, need python to be installed |
||||||
|
if (!prerequisiteHelper.IsPythonInstalled) |
||||||
|
{ |
||||||
|
MigrateProgressText = "Preparing Environment"; |
||||||
|
await prerequisiteHelper.InstallPythonIfNecessary(); |
||||||
|
} |
||||||
|
|
||||||
|
var libraryPath = settingsManager.LibraryDir; |
||||||
|
var oldPackages = settingsManager.GetOldInstalledPackages().ToArray(); |
||||||
|
|
||||||
|
foreach (var package in oldPackages) |
||||||
|
{ |
||||||
|
MigrateProgressCount++; |
||||||
|
#pragma warning disable CS0618 |
||||||
|
Logger.Info($"Migrating package {MigrateProgressCount} of {oldPackages.Length} at path {package.Path}"); |
||||||
|
#pragma warning restore CS0618 |
||||||
|
await package.MigratePath(); |
||||||
|
|
||||||
|
// Save after each step in case interrupted |
||||||
|
settingsManager.SaveSettings(); |
||||||
|
|
||||||
|
// Also recreate the venv |
||||||
|
var venvPath = Path.Combine(libraryPath, package.FullPath!); |
||||||
|
var venv = new PyVenvRunner(venvPath); |
||||||
|
await venv.Setup(existsOk: true); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,151 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Text.Json; |
||||||
|
using System.Text.Json.Serialization; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using AsyncAwaitBestPractices; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using CommunityToolkit.Mvvm.Input; |
||||||
|
using NLog; |
||||||
|
using Ookii.Dialogs.Wpf; |
||||||
|
using StabilityMatrix.Helper; |
||||||
|
using StabilityMatrix.Models; |
||||||
|
using StabilityMatrix.Models.Settings; |
||||||
|
|
||||||
|
namespace StabilityMatrix.ViewModels; |
||||||
|
|
||||||
|
public partial class SelectInstallLocationsViewModel : ObservableObject |
||||||
|
{ |
||||||
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||||
|
|
||||||
|
private const string ValidExistingDirectoryText = "Valid existing data directory found"; |
||||||
|
private const string InvalidDirectoryText = |
||||||
|
"Directory must be empty or have a valid settings.json file"; |
||||||
|
|
||||||
|
[ObservableProperty] private string dataDirectory; |
||||||
|
[ObservableProperty] private bool isPortableMode; |
||||||
|
|
||||||
|
[ObservableProperty] private string directoryStatusText = string.Empty; |
||||||
|
[ObservableProperty] private bool isStatusBadgeVisible; |
||||||
|
[ObservableProperty] private bool isDirectoryValid; |
||||||
|
|
||||||
|
public RefreshBadgeViewModel RefreshBadgeViewModel { get; } = new() |
||||||
|
{ |
||||||
|
State = ProgressState.Inactive, |
||||||
|
SuccessToolTipText = ValidExistingDirectoryText, |
||||||
|
FailToolTipText = InvalidDirectoryText |
||||||
|
}; |
||||||
|
|
||||||
|
public string DefaultInstallLocation => Path.Combine( |
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StabilityMatrix"); |
||||||
|
|
||||||
|
public SelectInstallLocationsViewModel() |
||||||
|
{ |
||||||
|
DataDirectory = DefaultInstallLocation; |
||||||
|
|
||||||
|
RefreshBadgeViewModel.RefreshFunc = ValidateDataDirectory; |
||||||
|
} |
||||||
|
|
||||||
|
public void OnLoaded() |
||||||
|
{ |
||||||
|
RefreshBadgeViewModel.RefreshCommand.ExecuteAsync(null).SafeFireAndForget(); |
||||||
|
} |
||||||
|
|
||||||
|
// Revalidate on data directory change |
||||||
|
// ReSharper disable once UnusedParameterInPartialMethod |
||||||
|
partial void OnDataDirectoryChanged(string value) |
||||||
|
{ |
||||||
|
RefreshBadgeViewModel.RefreshCommand.ExecuteAsync(null).SafeFireAndForget(); |
||||||
|
} |
||||||
|
|
||||||
|
// Validates current data directory |
||||||
|
private async Task<bool> ValidateDataDirectory() |
||||||
|
{ |
||||||
|
await using var delay = new MinimumDelay(100, 200); |
||||||
|
|
||||||
|
// Doesn't exist, this is fine as a new install, hide badge |
||||||
|
if (!Directory.Exists(DataDirectory)) |
||||||
|
{ |
||||||
|
IsStatusBadgeVisible = false; |
||||||
|
IsDirectoryValid = true; |
||||||
|
return true; |
||||||
|
} |
||||||
|
// Otherwise check that a settings.json exists |
||||||
|
var settingsPath = Path.Combine(DataDirectory, "settings.json"); |
||||||
|
|
||||||
|
// settings.json exists: Try deserializing it |
||||||
|
if (File.Exists(settingsPath)) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var jsonText = await File.ReadAllTextAsync(settingsPath); |
||||||
|
var _ = JsonSerializer.Deserialize<Settings>(jsonText, new JsonSerializerOptions |
||||||
|
{ |
||||||
|
Converters = { new JsonStringEnumConverter() } |
||||||
|
}); |
||||||
|
// If successful, show existing badge |
||||||
|
IsStatusBadgeVisible = true; |
||||||
|
IsDirectoryValid = true; |
||||||
|
DirectoryStatusText = ValidExistingDirectoryText; |
||||||
|
return true; |
||||||
|
} |
||||||
|
catch (Exception e) |
||||||
|
{ |
||||||
|
Logger.Info("Failed to deserialize settings.json: {Msg}", e.Message); |
||||||
|
// If not, show error badge, and set directory to invalid to prevent continuing |
||||||
|
IsStatusBadgeVisible = true; |
||||||
|
IsDirectoryValid = false; |
||||||
|
DirectoryStatusText = InvalidDirectoryText; |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// No settings.json |
||||||
|
|
||||||
|
// Check if the directory is %APPDATA%\StabilityMatrix: hide badge and set directory valid |
||||||
|
if (DataDirectory == DefaultInstallLocation) |
||||||
|
{ |
||||||
|
IsStatusBadgeVisible = false; |
||||||
|
IsDirectoryValid = true; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the directory is empty: hide badge and set directory to valid |
||||||
|
var isEmpty = !Directory.EnumerateFileSystemEntries(DataDirectory).Any(); |
||||||
|
if (isEmpty) |
||||||
|
{ |
||||||
|
IsStatusBadgeVisible = false; |
||||||
|
IsDirectoryValid = true; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// Not empty and not appdata: show error badge, and set directory to invalid |
||||||
|
IsStatusBadgeVisible = true; |
||||||
|
IsDirectoryValid = false; |
||||||
|
DirectoryStatusText = InvalidDirectoryText; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private void ShowFolderBrowserDialog() |
||||||
|
{ |
||||||
|
var dialog = new VistaFolderBrowserDialog |
||||||
|
{ |
||||||
|
Description = "Select a folder", |
||||||
|
UseDescriptionForTitle = true |
||||||
|
}; |
||||||
|
if (dialog.ShowDialog() != true) return; |
||||||
|
var path = dialog.SelectedPath; |
||||||
|
if (path == null) return; |
||||||
|
|
||||||
|
DataDirectory = path; |
||||||
|
} |
||||||
|
|
||||||
|
partial void OnIsPortableModeChanged(bool value) |
||||||
|
{ |
||||||
|
DataDirectory = value |
||||||
|
? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data") |
||||||
|
: DefaultInstallLocation; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,79 @@ |
|||||||
|
using System; |
||||||
|
using System.Diagnostics; |
||||||
|
using System.Net.Http; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using System.Windows; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using CommunityToolkit.Mvvm.Input; |
||||||
|
using StabilityMatrix.Helper; |
||||||
|
using StabilityMatrix.Models; |
||||||
|
using Wpf.Ui.Controls.Window; |
||||||
|
|
||||||
|
namespace StabilityMatrix.ViewModels; |
||||||
|
|
||||||
|
public partial class UpdateWindowViewModel : ObservableObject |
||||||
|
{ |
||||||
|
private readonly ISettingsManager settingsManager; |
||||||
|
private readonly IHttpClientFactory httpClientFactory; |
||||||
|
private readonly IUpdateHelper updateHelper; |
||||||
|
|
||||||
|
public UpdateWindowViewModel(ISettingsManager settingsManager, |
||||||
|
IHttpClientFactory httpClientFactory, IUpdateHelper updateHelper) |
||||||
|
{ |
||||||
|
this.settingsManager = settingsManager; |
||||||
|
this.httpClientFactory = httpClientFactory; |
||||||
|
this.updateHelper = updateHelper; |
||||||
|
} |
||||||
|
|
||||||
|
[ObservableProperty] private string? releaseNotes; |
||||||
|
[ObservableProperty] private string? updateText; |
||||||
|
[ObservableProperty] private int progressValue; |
||||||
|
[ObservableProperty] private bool showProgressBar; |
||||||
|
|
||||||
|
|
||||||
|
public UpdateInfo? UpdateInfo { get; set; } |
||||||
|
public WindowBackdropType WindowBackdropType => settingsManager.Settings.WindowBackdropType ?? |
||||||
|
WindowBackdropType.Mica; |
||||||
|
|
||||||
|
public async Task OnLoaded() |
||||||
|
{ |
||||||
|
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); |
||||||
|
Application.Current.Shutdown(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,79 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using Microsoft.Web.WebView2.Core; |
||||||
|
using NLog; |
||||||
|
|
||||||
|
namespace StabilityMatrix.ViewModels; |
||||||
|
|
||||||
|
public record struct NavigationResult(Uri? Uri, List<CoreWebView2Cookie>? Cookies); |
||||||
|
|
||||||
|
public partial class WebLoginViewModel : ObservableObject |
||||||
|
{ |
||||||
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||||
|
|
||||||
|
// Login Url, set externally on dialog creation |
||||||
|
[ObservableProperty] private string? loginUrl; |
||||||
|
// Bound current url source |
||||||
|
[ObservableProperty] private Uri? currentUri; |
||||||
|
// Always true after first navigation completed |
||||||
|
[ObservableProperty] private bool isContentLoaded; |
||||||
|
|
||||||
|
// Events |
||||||
|
public event EventHandler<NavigationResult> NavigationCompleted = delegate { }; |
||||||
|
public event EventHandler<NavigationResult> SourceChanged = delegate { }; |
||||||
|
|
||||||
|
public void OnLoaded() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Called on navigation source changes. |
||||||
|
/// </summary> |
||||||
|
public void OnSourceChanged(Uri? source, List<CoreWebView2Cookie>? cookies) |
||||||
|
{ |
||||||
|
Logger.Debug($"WebView source changed to {source} ({cookies?.Count} cookies)"); |
||||||
|
SourceChanged.Invoke(this, new NavigationResult(source, cookies)); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Called on navigation completed. (After scrollbar patch) |
||||||
|
/// </summary> |
||||||
|
public void OnNavigationCompleted(Uri? uri) |
||||||
|
{ |
||||||
|
Logger.Debug($"WebView loaded: {uri}"); |
||||||
|
NavigationCompleted.Invoke(this, new NavigationResult(uri, null)); |
||||||
|
IsContentLoaded = true; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Waits for navigation to a specific uri |
||||||
|
/// </summary> |
||||||
|
public async Task WaitForNavigation(Uri uri, CancellationToken ct = default) |
||||||
|
{ |
||||||
|
Logger.Debug($"Waiting for navigation to {uri}"); |
||||||
|
|
||||||
|
var navigationTask = new TaskCompletionSource<bool>(); |
||||||
|
|
||||||
|
var handler = new EventHandler<NavigationResult>((_, result) => |
||||||
|
{ |
||||||
|
navigationTask.TrySetResult(true); |
||||||
|
}); |
||||||
|
|
||||||
|
NavigationCompleted += handler; |
||||||
|
try |
||||||
|
{ |
||||||
|
await using (ct.Register(() => navigationTask.TrySetCanceled())) |
||||||
|
{ |
||||||
|
CurrentUri = uri; |
||||||
|
await navigationTask.Task; |
||||||
|
} |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
NavigationCompleted -= handler; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
<ui:ContentDialog |
||||||
|
CloseButtonText="Close" |
||||||
|
DialogMaxHeight="750" |
||||||
|
DialogMaxWidth="600" |
||||||
|
Loaded="WebLoginDialog_OnLoaded" |
||||||
|
Title="Login" |
||||||
|
d:DataContext="{d:DesignInstance Type=viewModels:WebLoginViewModel, |
||||||
|
IsDesignTimeCreatable=True}" |
||||||
|
d:DesignHeight="650" |
||||||
|
d:DesignWidth="600" |
||||||
|
mc:Ignorable="d" |
||||||
|
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}" |
||||||
|
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}" |
||||||
|
x:Class="StabilityMatrix.WebLoginDialog" |
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||||
|
xmlns:converters="clr-namespace:StabilityMatrix.Converters" |
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||||
|
xmlns:local="clr-namespace:StabilityMatrix" |
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||||
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" |
||||||
|
xmlns:viewModels="clr-namespace:StabilityMatrix.ViewModels" |
||||||
|
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||||
|
|
||||||
|
<ui:ContentDialog.Resources> |
||||||
|
<!-- ReSharper disable once Xaml.StaticResourceNotResolved --> |
||||||
|
<Style BasedOn="{StaticResource {x:Type ui:ContentDialog}}" TargetType="{x:Type local:WebLoginDialog}"> |
||||||
|
<!-- Disable scrollbars on the dialog --> |
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" /> |
||||||
|
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /> |
||||||
|
<converters:ValueConverterGroup x:Key="InvertIsStringNullOrWhitespaceConverter"> |
||||||
|
<converters:IsStringNullOrWhitespaceConverter /> |
||||||
|
<converters:BoolNegationConverter /> |
||||||
|
</converters:ValueConverterGroup> |
||||||
|
</ui:ContentDialog.Resources> |
||||||
|
|
||||||
|
<Grid Margin="0,0,0,12"> |
||||||
|
<Border |
||||||
|
BorderBrush="Transparent" |
||||||
|
BorderThickness="8" |
||||||
|
CornerRadius="4" |
||||||
|
Padding="8"> |
||||||
|
|
||||||
|
<DockPanel |
||||||
|
Background="{DynamicResource ApplicationBackgroundBrush}" |
||||||
|
MaxHeight="600" |
||||||
|
MinHeight="500" |
||||||
|
MinWidth="400"> |
||||||
|
<!-- Content --> |
||||||
|
<wv2:WebView2 |
||||||
|
DefaultBackgroundColor="Transparent" |
||||||
|
MinHeight="500" |
||||||
|
Name="LoginWebView" |
||||||
|
NavigationCompleted="LoginWebView_OnNavigationCompleted" |
||||||
|
Source="{Binding CurrentUri, Mode=TwoWay}" |
||||||
|
SourceChanged="LoginWebView_OnSourceChanged" /> |
||||||
|
</DockPanel> |
||||||
|
|
||||||
|
</Border> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
</ui:ContentDialog> |
@ -0,0 +1,53 @@ |
|||||||
|
using System.Windows; |
||||||
|
using Microsoft.Web.WebView2.Core; |
||||||
|
using NLog; |
||||||
|
using StabilityMatrix.ViewModels; |
||||||
|
using Wpf.Ui.Contracts; |
||||||
|
using Wpf.Ui.Controls.ContentDialogControl; |
||||||
|
|
||||||
|
namespace StabilityMatrix; |
||||||
|
|
||||||
|
public partial class WebLoginDialog : ContentDialog |
||||||
|
{ |
||||||
|
private const string DisableScrollbarJs = |
||||||
|
@"document.querySelector('body').style.overflow='scroll';
|
||||||
|
var style=document.createElement('style');style.type='text/css'; |
||||||
|
style.innerHTML='::-webkit-scrollbar{display:none}'; |
||||||
|
document.getElementsByTagName('body')[0].appendChild(style)";
|
||||||
|
|
||||||
|
private readonly Microsoft.Web.WebView2.Wpf.WebView2 currentWebView; |
||||||
|
|
||||||
|
public WebLoginViewModel ViewModel { get; set; } |
||||||
|
|
||||||
|
public WebLoginDialog(IContentDialogService dialogService, WebLoginViewModel viewModel) : base( |
||||||
|
dialogService.GetContentPresenter()) |
||||||
|
{ |
||||||
|
InitializeComponent(); |
||||||
|
DataContext = viewModel; |
||||||
|
ViewModel = viewModel; |
||||||
|
currentWebView = LoginWebView; |
||||||
|
} |
||||||
|
|
||||||
|
// Pass through OnLoaded to ViewModel |
||||||
|
private void WebLoginDialog_OnLoaded(object sender, RoutedEventArgs e) => ViewModel.OnLoaded(); |
||||||
|
|
||||||
|
// On nav complete we run js to hide scrollbar while allowing scrolling |
||||||
|
private async void LoginWebView_OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) |
||||||
|
{ |
||||||
|
if (e.IsSuccess) |
||||||
|
{ |
||||||
|
await currentWebView.ExecuteScriptAsync(DisableScrollbarJs); |
||||||
|
} |
||||||
|
// Pass to ViewModel event |
||||||
|
ViewModel.OnNavigationCompleted(currentWebView.Source); |
||||||
|
} |
||||||
|
|
||||||
|
// This happens before OnNavigationCompleted |
||||||
|
private async void LoginWebView_OnSourceChanged(object? sender, CoreWebView2SourceChangedEventArgs e) |
||||||
|
{ |
||||||
|
var url = currentWebView.Source; |
||||||
|
var cookies = url is null ? null : |
||||||
|
await currentWebView.CoreWebView2.CookieManager.GetCookiesAsync(url.AbsoluteUri); |
||||||
|
ViewModel.OnSourceChanged(currentWebView.Source, cookies); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue