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