Ionite
1 year ago
84 changed files with 5020 additions and 929 deletions
@ -0,0 +1,408 @@
|
||||
// Modified from https://github.com/AvaloniaUI/AvaloniaAutoGrid |
||||
/*The MIT License (MIT) |
||||
|
||||
Copyright (c) 2013 Charles Brown (carbonrobot) |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||
the Software, and to permit persons to whom the Software is furnished to do so, |
||||
subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/ |
||||
|
||||
using System; |
||||
using System.ComponentModel; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Linq; |
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Data; |
||||
using Avalonia.Layout; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
/// <summary> |
||||
/// Defines a flexible grid area that consists of columns and rows. |
||||
/// Depending on the orientation, either the rows or the columns are auto-generated, |
||||
/// and the children's position is set according to their index. |
||||
/// </summary> |
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] |
||||
public class AutoGrid : Grid |
||||
{ |
||||
/// <summary> |
||||
/// Gets or sets the child horizontal alignment. |
||||
/// </summary> |
||||
/// <value>The child horizontal alignment.</value> |
||||
[Category("Layout"), Description("Presets the horizontal alignment of all child controls")] |
||||
public HorizontalAlignment? ChildHorizontalAlignment |
||||
{ |
||||
get => (HorizontalAlignment?)GetValue(ChildHorizontalAlignmentProperty); |
||||
set => SetValue(ChildHorizontalAlignmentProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the child margin. |
||||
/// </summary> |
||||
/// <value>The child margin.</value> |
||||
[Category("Layout"), Description("Presets the margin of all child controls")] |
||||
public Thickness? ChildMargin |
||||
{ |
||||
get => (Thickness?)GetValue(ChildMarginProperty); |
||||
set => SetValue(ChildMarginProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the child vertical alignment. |
||||
/// </summary> |
||||
/// <value>The child vertical alignment.</value> |
||||
[Category("Layout"), Description("Presets the vertical alignment of all child controls")] |
||||
public VerticalAlignment? ChildVerticalAlignment |
||||
{ |
||||
get => (VerticalAlignment?)GetValue(ChildVerticalAlignmentProperty); |
||||
set => SetValue(ChildVerticalAlignmentProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the column count |
||||
/// </summary> |
||||
[Category("Layout"), Description("Defines a set number of columns")] |
||||
public int ColumnCount |
||||
{ |
||||
get => (int)GetValue(ColumnCountProperty)!; |
||||
set => SetValue(ColumnCountProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the fixed column width |
||||
/// </summary> |
||||
[Category("Layout"), Description("Presets the width of all columns set using the ColumnCount property")] |
||||
|
||||
public GridLength ColumnWidth |
||||
{ |
||||
get => (GridLength)GetValue(ColumnWidthProperty)!; |
||||
set => SetValue(ColumnWidthProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets a value indicating whether the children are automatically indexed. |
||||
/// <remarks> |
||||
/// The default is <c>true</c>. |
||||
/// Note that if children are already indexed, setting this property to <c>false</c> will not remove their indices. |
||||
/// </remarks> |
||||
/// </summary> |
||||
[Category("Layout"), Description("Set to false to disable the auto layout functionality")] |
||||
public bool IsAutoIndexing |
||||
{ |
||||
get => (bool)GetValue(IsAutoIndexingProperty)!; |
||||
set => SetValue(IsAutoIndexingProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the orientation. |
||||
/// <remarks>The default is Vertical.</remarks> |
||||
/// </summary> |
||||
/// <value>The orientation.</value> |
||||
[Category("Layout"), Description("Defines the directionality of the autolayout. Use vertical for a column first layout, horizontal for a row first layout.")] |
||||
public Orientation Orientation |
||||
{ |
||||
get => (Orientation)GetValue(OrientationProperty)!; |
||||
set => SetValue(OrientationProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the number of rows |
||||
/// </summary> |
||||
[Category("Layout"), Description("Defines a set number of rows")] |
||||
public int RowCount |
||||
{ |
||||
get => (int)GetValue(RowCountProperty)!; |
||||
set => SetValue(RowCountProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the fixed row height |
||||
/// </summary> |
||||
[Category("Layout"), Description("Presets the height of all rows set using the RowCount property")] |
||||
public GridLength RowHeight |
||||
{ |
||||
get => (GridLength)GetValue(RowHeightProperty)!; |
||||
set => SetValue(RowHeightProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Handles the column count changed event |
||||
/// </summary> |
||||
public static void ColumnCountChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
if ((int)e.NewValue! < 0) |
||||
return; |
||||
|
||||
var grid = (AutoGrid)e.Sender; |
||||
|
||||
|
||||
// look for an existing column definition for the height |
||||
var width = grid.ColumnWidth; |
||||
if (!grid.IsSet(ColumnWidthProperty) && grid.ColumnDefinitions.Count > 0) |
||||
width = grid.ColumnDefinitions[0].Width; |
||||
|
||||
// clear and rebuild |
||||
grid.ColumnDefinitions.Clear(); |
||||
for (var i = 0; i < (int)e.NewValue; i++) |
||||
grid.ColumnDefinitions.Add( |
||||
new ColumnDefinition() { Width = width }); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Handle the fixed column width changed event |
||||
/// </summary> |
||||
public static void FixedColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
var grid = (AutoGrid)e.Sender; |
||||
|
||||
// add a default column if missing |
||||
if (grid.ColumnDefinitions.Count == 0) |
||||
grid.ColumnDefinitions.Add(new ColumnDefinition()); |
||||
|
||||
// set all existing columns to this width |
||||
foreach (var t in grid.ColumnDefinitions) |
||||
t.Width = (GridLength)e.NewValue!; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Handle the fixed row height changed event |
||||
/// </summary> |
||||
public static void FixedRowHeightChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
var grid = (AutoGrid)e.Sender; |
||||
|
||||
// add a default row if missing |
||||
if (grid.RowDefinitions.Count == 0) |
||||
grid.RowDefinitions.Add(new RowDefinition()); |
||||
|
||||
// set all existing rows to this height |
||||
foreach (var t in grid.RowDefinitions) |
||||
t.Height = (GridLength)e.NewValue!; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Handles the row count changed event |
||||
/// </summary> |
||||
public static void RowCountChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
if ((int)e.NewValue! < 0) |
||||
return; |
||||
|
||||
var grid = (AutoGrid)e.Sender; |
||||
|
||||
// look for an existing row to get the height |
||||
var height = grid.RowHeight; |
||||
if (!grid.IsSet(RowHeightProperty) && grid.RowDefinitions.Count > 0) |
||||
height = grid.RowDefinitions[0].Height; |
||||
|
||||
// clear and rebuild |
||||
grid.RowDefinitions.Clear(); |
||||
for (var i = 0; i < (int)e.NewValue; i++) |
||||
grid.RowDefinitions.Add( |
||||
new RowDefinition() { Height = height }); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Called when [child horizontal alignment changed]. |
||||
/// </summary> |
||||
private static void OnChildHorizontalAlignmentChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
var grid = (AutoGrid)e.Sender; |
||||
foreach (var child in grid.Children) |
||||
{ |
||||
child.SetValue(HorizontalAlignmentProperty, |
||||
grid.ChildHorizontalAlignment ?? AvaloniaProperty.UnsetValue); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Called when [child layout changed]. |
||||
/// </summary> |
||||
private static void OnChildMarginChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
var grid = (AutoGrid)e.Sender; |
||||
foreach (var child in grid.Children) |
||||
{ |
||||
child.SetValue(MarginProperty, grid.ChildMargin ?? AvaloniaProperty.UnsetValue); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Called when [child vertical alignment changed]. |
||||
/// </summary> |
||||
private static void OnChildVerticalAlignmentChanged(AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
var grid = (AutoGrid)e.Sender; |
||||
foreach (var child in grid.Children) |
||||
{ |
||||
child.SetValue(VerticalAlignmentProperty, grid.ChildVerticalAlignment ?? AvaloniaProperty.UnsetValue); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Apply child margins and layout effects such as alignment |
||||
/// </summary> |
||||
private void ApplyChildLayout(Control child) |
||||
{ |
||||
if (ChildMargin != null) |
||||
{ |
||||
child.SetValue(MarginProperty, ChildMargin.Value, BindingPriority.Template); |
||||
} |
||||
if (ChildHorizontalAlignment != null) |
||||
{ |
||||
child.SetValue(HorizontalAlignmentProperty, ChildHorizontalAlignment.Value, BindingPriority.Template); |
||||
} |
||||
if (ChildVerticalAlignment != null) |
||||
{ |
||||
child.SetValue(VerticalAlignmentProperty, ChildVerticalAlignment.Value, BindingPriority.Template); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Clamp a value to its maximum. |
||||
/// </summary> |
||||
private int Clamp(int value, int max) |
||||
{ |
||||
return (value > max) ? max : value; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Perform the grid layout of row and column indexes |
||||
/// </summary> |
||||
private void PerformLayout() |
||||
{ |
||||
var fillRowFirst = Orientation == Orientation.Horizontal; |
||||
var rowCount = RowDefinitions.Count; |
||||
var colCount = ColumnDefinitions.Count; |
||||
|
||||
if (rowCount == 0 || colCount == 0) |
||||
return; |
||||
|
||||
var position = 0; |
||||
var skip = new bool[rowCount, colCount]; |
||||
foreach (var child in Children.OfType<Control>()) |
||||
{ |
||||
var childIsCollapsed = !child.IsVisible; |
||||
if (IsAutoIndexing && !childIsCollapsed) |
||||
{ |
||||
if (fillRowFirst) |
||||
{ |
||||
var row = Clamp(position / colCount, rowCount - 1); |
||||
var col = Clamp(position % colCount, colCount - 1); |
||||
if (skip[row, col]) |
||||
{ |
||||
position++; |
||||
row = (position / colCount); |
||||
col = (position % colCount); |
||||
} |
||||
|
||||
SetRow(child, row); |
||||
SetColumn(child, col); |
||||
position += GetColumnSpan(child); |
||||
|
||||
var offset = GetRowSpan(child) - 1; |
||||
while (offset > 0) |
||||
{ |
||||
skip[row + offset--, col] = true; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
var row = Clamp(position % rowCount, rowCount - 1); |
||||
var col = Clamp(position / rowCount, colCount - 1); |
||||
if (skip[row, col]) |
||||
{ |
||||
position++; |
||||
row = position % rowCount; |
||||
col = position / rowCount; |
||||
} |
||||
|
||||
SetRow(child, row); |
||||
SetColumn(child, col); |
||||
position += GetRowSpan(child); |
||||
|
||||
var offset = GetColumnSpan(child) - 1; |
||||
while (offset > 0) |
||||
{ |
||||
skip[row, col + offset--] = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
ApplyChildLayout(child); |
||||
} |
||||
} |
||||
|
||||
public static readonly AvaloniaProperty<HorizontalAlignment?> ChildHorizontalAlignmentProperty = |
||||
AvaloniaProperty.Register<AutoGrid, HorizontalAlignment?>("ChildHorizontalAlignment"); |
||||
|
||||
public static readonly AvaloniaProperty<Thickness?> ChildMarginProperty = |
||||
AvaloniaProperty.Register<AutoGrid, Thickness?>("ChildMargin"); |
||||
|
||||
public static readonly AvaloniaProperty<VerticalAlignment?> ChildVerticalAlignmentProperty = |
||||
AvaloniaProperty.Register<AutoGrid, VerticalAlignment?>("ChildVerticalAlignment"); |
||||
|
||||
public static readonly AvaloniaProperty<int> ColumnCountProperty = |
||||
AvaloniaProperty.RegisterAttached<Control, int>("ColumnCount", typeof(AutoGrid), 1); |
||||
|
||||
public static readonly AvaloniaProperty<GridLength> ColumnWidthProperty = |
||||
AvaloniaProperty.RegisterAttached<Control, GridLength>("ColumnWidth", typeof(AutoGrid), GridLength.Auto); |
||||
|
||||
public static readonly AvaloniaProperty<bool> IsAutoIndexingProperty = |
||||
AvaloniaProperty.Register<AutoGrid, bool>("IsAutoIndexing", true); |
||||
|
||||
public static readonly AvaloniaProperty<Orientation> OrientationProperty = |
||||
AvaloniaProperty.Register<AutoGrid, Orientation>("Orientation", Orientation.Vertical); |
||||
|
||||
public static readonly AvaloniaProperty<int> RowCountProperty = |
||||
AvaloniaProperty.RegisterAttached<Control, int>("RowCount", typeof(AutoGrid), 1); |
||||
|
||||
public static readonly AvaloniaProperty<GridLength> RowHeightProperty = |
||||
AvaloniaProperty.RegisterAttached<Control, GridLength>("RowHeight", typeof(AutoGrid), GridLength.Auto); |
||||
|
||||
static AutoGrid() |
||||
{ |
||||
AffectsMeasure<AutoGrid>(ChildHorizontalAlignmentProperty, ChildMarginProperty, |
||||
ChildVerticalAlignmentProperty, ColumnCountProperty, ColumnWidthProperty, IsAutoIndexingProperty, OrientationProperty, |
||||
RowHeightProperty); |
||||
|
||||
ChildHorizontalAlignmentProperty.Changed.Subscribe(OnChildHorizontalAlignmentChanged); |
||||
ChildMarginProperty.Changed.Subscribe(OnChildMarginChanged); |
||||
ChildVerticalAlignmentProperty.Changed.Subscribe(OnChildVerticalAlignmentChanged); |
||||
ColumnCountProperty.Changed.Subscribe(ColumnCountChanged); |
||||
RowCountProperty.Changed.Subscribe(RowCountChanged); |
||||
ColumnWidthProperty.Changed.Subscribe(FixedColumnWidthChanged); |
||||
RowHeightProperty.Changed.Subscribe(FixedRowHeightChanged); |
||||
} |
||||
|
||||
#region Overrides |
||||
|
||||
/// <summary> |
||||
/// Measures the children of a <see cref="T:System.Windows.Controls.Grid"/> in anticipation of arranging them during the <see cref="M:ArrangeOverride"/> pass. |
||||
/// </summary> |
||||
/// <param name="constraint">Indicates an upper limit size that should not be exceeded.</param> |
||||
/// <returns> |
||||
/// <see cref="Size"/> that represents the required size to arrange child content. |
||||
/// </returns> |
||||
protected override Size MeasureOverride(Size constraint) |
||||
{ |
||||
PerformLayout(); |
||||
return base.MeasureOverride(constraint); |
||||
} |
||||
|
||||
#endregion Overrides |
||||
} |
@ -0,0 +1,65 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Avalonia.DesignData; |
||||
|
||||
public class MockDownloadProgressItemViewModel : PausableProgressItemViewModelBase |
||||
{ |
||||
private Task? dummyTask; |
||||
private CancellationTokenSource? cts; |
||||
|
||||
public MockDownloadProgressItemViewModel(string fileName) |
||||
{ |
||||
Name = fileName; |
||||
Progress.Value = 5; |
||||
Progress.IsIndeterminate = false; |
||||
Progress.Text = "Downloading..."; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Cancel() |
||||
{ |
||||
// Cancel the task that updates progress |
||||
cts?.Cancel(); |
||||
cts = null; |
||||
dummyTask = null; |
||||
|
||||
State = ProgressState.Cancelled; |
||||
Progress.Text = "Cancelled"; |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Pause() |
||||
{ |
||||
// Cancel the task that updates progress |
||||
cts?.Cancel(); |
||||
cts = null; |
||||
dummyTask = null; |
||||
|
||||
State = ProgressState.Inactive; |
||||
|
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Resume() |
||||
{ |
||||
// Start a task that updates progress every 100ms |
||||
cts = new CancellationTokenSource(); |
||||
dummyTask = Task.Run(async () => |
||||
{ |
||||
while (State != ProgressState.Success) |
||||
{ |
||||
await Task.Delay(100, cts.Token); |
||||
Progress.Value += 1; |
||||
} |
||||
}, cts.Token); |
||||
|
||||
State = ProgressState.Working; |
||||
|
||||
return Task.CompletedTask; |
||||
} |
||||
} |
@ -0,0 +1,22 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.DesignData; |
||||
|
||||
public class MockTrackedDownloadService : ITrackedDownloadService |
||||
{ |
||||
/// <inheritdoc /> |
||||
public IEnumerable<TrackedDownload> Downloads => Array.Empty<TrackedDownload>(); |
||||
|
||||
/// <inheritdoc /> |
||||
public event EventHandler<TrackedDownload>? DownloadAdded; |
||||
|
||||
/// <inheritdoc /> |
||||
public TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
} |
@ -0,0 +1,91 @@
|
||||
using System; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.IO; |
||||
using System.Threading.Tasks; |
||||
using Microsoft.Extensions.Logging; |
||||
using Polly; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Extensions; |
||||
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] |
||||
public static class DirectoryPathExtensions |
||||
{ |
||||
/// <summary> |
||||
/// Deletes a directory and all of its contents recursively. |
||||
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff. |
||||
/// </summary> |
||||
public static Task DeleteVerboseAsync(this DirectoryPath directory, ILogger? logger = default) |
||||
{ |
||||
var policy = Policy.Handle<IOException>() |
||||
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)), |
||||
onRetry: (exception, calculatedWaitDuration) => |
||||
{ |
||||
logger?.LogWarning( |
||||
exception, |
||||
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}", |
||||
directory, calculatedWaitDuration); |
||||
}); |
||||
|
||||
return policy.ExecuteAsync(async () => |
||||
{ |
||||
await Task.Run(() => { DeleteVerbose(directory, logger); }); |
||||
}); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Deletes a directory and all of its contents recursively. |
||||
/// Removes link targets without deleting the source. |
||||
/// </summary> |
||||
public static void DeleteVerbose(this DirectoryPath directory, ILogger? logger = default) |
||||
{ |
||||
// Skip if directory does not exist |
||||
if (!directory.Exists) |
||||
{ |
||||
return; |
||||
} |
||||
// For junction points, delete with recursive false |
||||
if (directory.IsSymbolicLink) |
||||
{ |
||||
logger?.LogInformation("Removing junction point {TargetDirectory}", directory); |
||||
try |
||||
{ |
||||
directory.Delete(false); |
||||
return; |
||||
} |
||||
catch (IOException ex) |
||||
{ |
||||
throw new IOException($"Failed to delete junction point {directory}", ex); |
||||
} |
||||
} |
||||
// Recursively delete all subdirectories |
||||
foreach (var subDir in directory.Info.EnumerateDirectories()) |
||||
{ |
||||
DeleteVerbose(subDir, logger); |
||||
} |
||||
|
||||
// Delete all files in the directory |
||||
foreach (var filePath in directory.Info.EnumerateFiles()) |
||||
{ |
||||
try |
||||
{ |
||||
filePath.Attributes = FileAttributes.Normal; |
||||
filePath.Delete(); |
||||
} |
||||
catch (IOException ex) |
||||
{ |
||||
throw new IOException($"Failed to delete file {filePath.FullName}", ex); |
||||
} |
||||
} |
||||
|
||||
// Delete this directory |
||||
try |
||||
{ |
||||
directory.Delete(false); |
||||
} |
||||
catch (IOException ex) |
||||
{ |
||||
throw new IOException($"Failed to delete directory {directory}", ex); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic; |
||||
using System.Collections.Immutable; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Globalization; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Languages; |
||||
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] |
||||
public static class Cultures |
||||
{ |
||||
public static CultureInfo Default { get; } = new("en-US"); |
||||
|
||||
public static CultureInfo Current => Resources.Culture; |
||||
|
||||
public static readonly Dictionary<string, CultureInfo> SupportedCulturesByCode = |
||||
new Dictionary<string, CultureInfo> |
||||
{ |
||||
["en-US"] = Default, |
||||
["ja-JP"] = new("ja-JP") |
||||
}; |
||||
|
||||
public static IReadOnlyList<CultureInfo> SupportedCultures |
||||
=> SupportedCulturesByCode.Values.ToImmutableList(); |
||||
|
||||
public static CultureInfo GetSupportedCultureOrDefault(string? cultureCode) |
||||
{ |
||||
if (cultureCode is null |
||||
|| !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) |
||||
{ |
||||
return Default; |
||||
} |
||||
|
||||
return culture; |
||||
} |
||||
|
||||
public static bool TrySetSupportedCulture(string? cultureCode) |
||||
{ |
||||
if (cultureCode is null |
||||
|| !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
Resources.Culture = culture; |
||||
return true; |
||||
} |
||||
|
||||
public static bool TrySetSupportedCulture(CultureInfo? cultureInfo) |
||||
{ |
||||
return cultureInfo is not null && TrySetSupportedCulture(cultureInfo.Name); |
||||
} |
||||
} |
@ -0,0 +1,206 @@
|
||||
//------------------------------------------------------------------------------ |
||||
// <auto-generated> |
||||
// This code was generated by a tool. |
||||
// |
||||
// Changes to this file may cause incorrect behavior and will be lost if |
||||
// the code is regenerated. |
||||
// </auto-generated> |
||||
//------------------------------------------------------------------------------ |
||||
|
||||
namespace StabilityMatrix.Avalonia.Languages { |
||||
using System; |
||||
|
||||
|
||||
/// <summary> |
||||
/// A strongly-typed resource class, for looking up localized strings, etc. |
||||
/// </summary> |
||||
// This class was auto-generated by the StronglyTypedResourceBuilder |
||||
// class via a tool like ResGen or Visual Studio. |
||||
// To add or remove a member, edit your .ResX file then rerun ResGen |
||||
// with the /str option, or rebuild your VS project. |
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] |
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] |
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] |
||||
public class Resources { |
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan; |
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture; |
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
||||
internal Resources() { |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Returns the cached ResourceManager instance used by this class. |
||||
/// </summary> |
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] |
||||
public static global::System.Resources.ResourceManager ResourceManager { |
||||
get { |
||||
if (object.ReferenceEquals(resourceMan, null)) { |
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("StabilityMatrix.Avalonia.Languages.Resources", typeof(Resources).Assembly); |
||||
resourceMan = temp; |
||||
} |
||||
return resourceMan; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Overrides the current thread's CurrentUICulture property for all |
||||
/// resource lookups using this strongly typed resource class. |
||||
/// </summary> |
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] |
||||
public static global::System.Globalization.CultureInfo Culture { |
||||
get { |
||||
return resourceCulture; |
||||
} |
||||
set { |
||||
resourceCulture = value; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Cancel. |
||||
/// </summary> |
||||
public static string Action_Cancel { |
||||
get { |
||||
return ResourceManager.GetString("Action_Cancel", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Import. |
||||
/// </summary> |
||||
public static string Action_Import { |
||||
get { |
||||
return ResourceManager.GetString("Action_Import", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Launch. |
||||
/// </summary> |
||||
public static string Action_Launch { |
||||
get { |
||||
return ResourceManager.GetString("Action_Launch", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Quit. |
||||
/// </summary> |
||||
public static string Action_Quit { |
||||
get { |
||||
return ResourceManager.GetString("Action_Quit", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Relaunch. |
||||
/// </summary> |
||||
public static string Action_Relaunch { |
||||
get { |
||||
return ResourceManager.GetString("Action_Relaunch", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Relaunch Later. |
||||
/// </summary> |
||||
public static string Action_RelaunchLater { |
||||
get { |
||||
return ResourceManager.GetString("Action_RelaunchLater", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Save. |
||||
/// </summary> |
||||
public static string Action_Save { |
||||
get { |
||||
return ResourceManager.GetString("Action_Save", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Branches. |
||||
/// </summary> |
||||
public static string Label_Branches { |
||||
get { |
||||
return ResourceManager.GetString("Label_Branches", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Language. |
||||
/// </summary> |
||||
public static string Label_Language { |
||||
get { |
||||
return ResourceManager.GetString("Label_Language", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Package Type. |
||||
/// </summary> |
||||
public static string Label_PackageType { |
||||
get { |
||||
return ResourceManager.GetString("Label_PackageType", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Relaunch Required. |
||||
/// </summary> |
||||
public static string Label_RelaunchRequired { |
||||
get { |
||||
return ResourceManager.GetString("Label_RelaunchRequired", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Releases. |
||||
/// </summary> |
||||
public static string Label_Releases { |
||||
get { |
||||
return ResourceManager.GetString("Label_Releases", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Unknown Package. |
||||
/// </summary> |
||||
public static string Label_UnknownPackage { |
||||
get { |
||||
return ResourceManager.GetString("Label_UnknownPackage", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Version. |
||||
/// </summary> |
||||
public static string Label_Version { |
||||
get { |
||||
return ResourceManager.GetString("Label_Version", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Version Type. |
||||
/// </summary> |
||||
public static string Label_VersionType { |
||||
get { |
||||
return ResourceManager.GetString("Label_VersionType", resourceCulture); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Looks up a localized string similar to Relaunch is required for new language option to take effect. |
||||
/// </summary> |
||||
public static string Text_RelaunchRequiredToApplyLanguage { |
||||
get { |
||||
return ResourceManager.GetString("Text_RelaunchRequiredToApplyLanguage", resourceCulture); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
<root> |
||||
<resheader name="resmimetype"> |
||||
<value>text/microsoft-resx</value> |
||||
</resheader> |
||||
<resheader name="version"> |
||||
<value>1.3</value> |
||||
</resheader> |
||||
<resheader name="reader"> |
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> |
||||
</resheader> |
||||
<resheader name="writer"> |
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> |
||||
</resheader> |
||||
<data name="Action_Save" xml:space="preserve"> |
||||
<value>保存</value> |
||||
</data> |
||||
<data name="Action_Cancel" xml:space="preserve"> |
||||
<value>戻る</value> |
||||
</data> |
||||
<data name="Label_Language" xml:space="preserve"> |
||||
<value>言語</value> |
||||
</data> |
||||
</root> |
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
|
||||
<root> |
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> |
||||
<xsd:element name="root" msdata:IsDataSet="true"> |
||||
|
||||
</xsd:element> |
||||
</xsd:schema> |
||||
<resheader name="resmimetype"> |
||||
<value>text/microsoft-resx</value> |
||||
</resheader> |
||||
<resheader name="version"> |
||||
<value>1.3</value> |
||||
</resheader> |
||||
<resheader name="reader"> |
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> |
||||
</resheader> |
||||
<resheader name="writer"> |
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> |
||||
</resheader> |
||||
<data name="Action_Launch" xml:space="preserve"> |
||||
<value>Launch</value> |
||||
</data> |
||||
<data name="Action_Quit" xml:space="preserve"> |
||||
<value>Quit</value> |
||||
</data> |
||||
<data name="Action_Save" xml:space="preserve"> |
||||
<value>Save</value> |
||||
</data> |
||||
<data name="Action_Cancel" xml:space="preserve"> |
||||
<value>Cancel</value> |
||||
</data> |
||||
<data name="Label_Language" xml:space="preserve"> |
||||
<value>Language</value> |
||||
</data> |
||||
<data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve"> |
||||
<value>Relaunch is required for new language option to take effect</value> |
||||
</data> |
||||
<data name="Action_Relaunch" xml:space="preserve"> |
||||
<value>Relaunch</value> |
||||
</data> |
||||
<data name="Action_RelaunchLater" xml:space="preserve"> |
||||
<value>Relaunch Later</value> |
||||
</data> |
||||
<data name="Label_RelaunchRequired" xml:space="preserve"> |
||||
<value>Relaunch Required</value> |
||||
</data> |
||||
<data name="Label_UnknownPackage" xml:space="preserve"> |
||||
<value>Unknown Package</value> |
||||
</data> |
||||
<data name="Action_Import" xml:space="preserve"> |
||||
<value>Import</value> |
||||
</data> |
||||
<data name="Label_PackageType" xml:space="preserve"> |
||||
<value>Package Type</value> |
||||
</data> |
||||
<data name="Label_Version" xml:space="preserve"> |
||||
<value>Version</value> |
||||
</data> |
||||
<data name="Label_VersionType" xml:space="preserve"> |
||||
<value>Version Type</value> |
||||
</data> |
||||
<data name="Label_Releases" xml:space="preserve"> |
||||
<value>Releases</value> |
||||
</data> |
||||
<data name="Label_Branches" xml:space="preserve"> |
||||
<value>Branches</value> |
||||
</data> |
||||
</root> |
@ -0,0 +1,347 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> |
||||
<Design.PreviewWith> |
||||
<Border Padding="20"> |
||||
<StackPanel> |
||||
<ToggleButton Classes="success" Content="Success Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Classes="accent" Content="FA Accent Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Classes="systemaccent" Content="System Accent Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Classes="danger" Content="Danger Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Classes="info" Content="Info Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Classes="transparent-info" Content="Semi-Transparent Info Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Classes="transparent" Content="Transparent Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Classes="transparent-full" Content="Transparent Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ToggleButton Content="Disabled Button" Margin="8" IsEnabled="False" HorizontalAlignment="Center" /> |
||||
</StackPanel> |
||||
</Border> |
||||
</Design.PreviewWith> |
||||
|
||||
<!-- Success --> |
||||
<Style Selector="ToggleButton.success"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeGreenColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeGreenColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkGreenColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkGreenColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkGreenColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkGreenColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Danger --> |
||||
<Style Selector="ToggleButton.danger"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeRedColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkRedColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Info --> |
||||
<Style Selector="ToggleButton.info"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeBlueColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!--Accent Button--> |
||||
<Style Selector="ToggleButton.accent"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- SystemAccent --> |
||||
<Style Selector="ToggleButton.systemaccent"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark1}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark1}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark2}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark2}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Transparent --> |
||||
<Style Selector="ToggleButton.transparent"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Semi-Transparent Info --> |
||||
<Style Selector="ToggleButton.transparent-info"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColorTransparent}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColorTransparent}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeBlueColorTransparent}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColorTransparent}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColorTransparent}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColorTransparent}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Full Transparent --> |
||||
<Style Selector="ToggleButton.transparent-full"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ ui|FABorder#Root"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
</Styles> |
@ -0,0 +1,49 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Threading.Tasks; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] |
||||
public abstract partial class PausableProgressItemViewModelBase : ProgressItemViewModelBase |
||||
{ |
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(IsPaused), nameof(IsCompleted), nameof(CanPauseResume), nameof(CanCancel))] |
||||
private ProgressState state = ProgressState.Inactive; |
||||
|
||||
/// <summary> |
||||
/// Whether the progress is paused |
||||
/// </summary> |
||||
public bool IsPaused => State == ProgressState.Inactive; |
||||
|
||||
/// <summary> |
||||
/// Whether the progress has succeeded, failed or was cancelled |
||||
/// </summary> |
||||
public override bool IsCompleted => State is ProgressState.Success or ProgressState.Failed or ProgressState.Cancelled; |
||||
|
||||
public virtual bool SupportsPauseResume => true; |
||||
public virtual bool SupportsCancel => true; |
||||
|
||||
public bool CanPauseResume => SupportsPauseResume && !IsCompleted; |
||||
public bool CanCancel => SupportsCancel && !IsCompleted; |
||||
|
||||
private AsyncRelayCommand? pauseCommand; |
||||
public IAsyncRelayCommand PauseCommand => pauseCommand ??= new AsyncRelayCommand(Pause); |
||||
public virtual Task Pause() => Task.CompletedTask; |
||||
|
||||
private AsyncRelayCommand? resumeCommand; |
||||
public IAsyncRelayCommand ResumeCommand => resumeCommand ??= new AsyncRelayCommand(Resume); |
||||
public virtual Task Resume() => Task.CompletedTask; |
||||
|
||||
private AsyncRelayCommand? cancelCommand; |
||||
public IAsyncRelayCommand CancelCommand => cancelCommand ??= new AsyncRelayCommand(Cancel); |
||||
public virtual Task Cancel() => Task.CompletedTask; |
||||
|
||||
[RelayCommand] |
||||
private Task TogglePauseResume() |
||||
{ |
||||
return IsPaused ? Resume() : Pause(); |
||||
} |
||||
} |
@ -0,0 +1,16 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
public abstract partial class ProgressItemViewModelBase : ViewModelBase |
||||
{ |
||||
[ObservableProperty] private Guid id; |
||||
[ObservableProperty] private string? name; |
||||
[ObservableProperty] private bool failed; |
||||
|
||||
public virtual bool IsCompleted => Progress.Value >= 100 || Failed; |
||||
|
||||
public ProgressViewModel Progress { get; } = new(); |
||||
} |
@ -0,0 +1,221 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.Immutable; |
||||
using System.Collections.ObjectModel; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Threading; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using NLog; |
||||
using StabilityMatrix.Avalonia.Views.Dialogs; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper.Factory; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Database; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Packages; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||
|
||||
[View(typeof(PackageImportDialog))] |
||||
public partial class PackageImportViewModel : ContentDialogViewModelBase |
||||
{ |
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||
|
||||
private readonly IPackageFactory packageFactory; |
||||
private readonly ISettingsManager settingsManager; |
||||
|
||||
[ObservableProperty] private DirectoryPath? packagePath; |
||||
[ObservableProperty] private BasePackage? selectedBasePackage; |
||||
|
||||
public IReadOnlyList<BasePackage> AvailablePackages |
||||
=> packageFactory.GetAllAvailablePackages().ToImmutableArray(); |
||||
|
||||
[ObservableProperty] private PackageVersion? selectedVersion; |
||||
|
||||
[ObservableProperty] private ObservableCollection<GitCommit>? availableCommits; |
||||
[ObservableProperty] private ObservableCollection<PackageVersion>? availableVersions; |
||||
|
||||
[ObservableProperty] private GitCommit? selectedCommit; |
||||
|
||||
// Version types (release or commit) |
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(ReleaseLabelText), |
||||
nameof(IsReleaseMode), nameof(SelectedVersion))] |
||||
private PackageVersionType selectedVersionType = PackageVersionType.Commit; |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))] |
||||
private PackageVersionType availableVersionTypes = |
||||
PackageVersionType.GithubRelease | PackageVersionType.Commit; |
||||
public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch"; |
||||
public bool IsReleaseMode |
||||
{ |
||||
get => SelectedVersionType == PackageVersionType.GithubRelease; |
||||
set => SelectedVersionType = value ? PackageVersionType.GithubRelease : PackageVersionType.Commit; |
||||
} |
||||
|
||||
public bool IsReleaseModeAvailable => AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease); |
||||
|
||||
public PackageImportViewModel( |
||||
IPackageFactory packageFactory, |
||||
ISettingsManager settingsManager) |
||||
{ |
||||
this.packageFactory = packageFactory; |
||||
this.settingsManager = settingsManager; |
||||
} |
||||
|
||||
public override async Task OnLoadedAsync() |
||||
{ |
||||
SelectedBasePackage ??= AvailablePackages[0]; |
||||
|
||||
if (Design.IsDesignMode) return; |
||||
// Populate available versions |
||||
try |
||||
{ |
||||
if (IsReleaseMode) |
||||
{ |
||||
var versions = (await SelectedBasePackage.GetAllVersions()).ToList(); |
||||
AvailableVersions = new ObservableCollection<PackageVersion>(versions); |
||||
if (!AvailableVersions.Any()) return; |
||||
|
||||
SelectedVersion = AvailableVersions[0]; |
||||
} |
||||
else |
||||
{ |
||||
var branches = (await SelectedBasePackage.GetAllBranches()).ToList(); |
||||
AvailableVersions = new ObservableCollection<PackageVersion>(branches.Select(b => |
||||
new PackageVersion |
||||
{ |
||||
TagName = b.Name, |
||||
ReleaseNotesMarkdown = b.Commit.Label |
||||
})); |
||||
UpdateSelectedVersionToLatestMain(); |
||||
} |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
Logger.Warn("Error getting versions: {Exception}", e.ToString()); |
||||
} |
||||
} |
||||
|
||||
private static string GetDisplayVersion(string version, string? branch) |
||||
{ |
||||
return branch == null ? version : $"{branch}@{version[..7]}"; |
||||
} |
||||
|
||||
// When available version types change, reset selected version type if not compatible |
||||
partial void OnAvailableVersionTypesChanged(PackageVersionType value) |
||||
{ |
||||
if (!value.HasFlag(SelectedVersionType)) |
||||
{ |
||||
SelectedVersionType = value; |
||||
} |
||||
} |
||||
|
||||
// When changing branch / release modes, refresh |
||||
// ReSharper disable once UnusedParameterInPartialMethod |
||||
partial void OnSelectedVersionTypeChanged(PackageVersionType value) |
||||
=> OnSelectedBasePackageChanged(SelectedBasePackage); |
||||
|
||||
partial void OnSelectedBasePackageChanged(BasePackage? value) |
||||
{ |
||||
if (value is null || SelectedBasePackage is null) |
||||
{ |
||||
AvailableVersions?.Clear(); |
||||
AvailableCommits?.Clear(); |
||||
return; |
||||
} |
||||
|
||||
AvailableVersions?.Clear(); |
||||
AvailableCommits?.Clear(); |
||||
|
||||
AvailableVersionTypes = SelectedBasePackage.ShouldIgnoreReleases |
||||
? PackageVersionType.Commit |
||||
: PackageVersionType.GithubRelease | PackageVersionType.Commit; |
||||
|
||||
if (Design.IsDesignMode) return; |
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () => |
||||
{ |
||||
Logger.Debug($"Release mode: {IsReleaseMode}"); |
||||
var versions = (await value.GetAllVersions(IsReleaseMode)).ToList(); |
||||
|
||||
if (!versions.Any()) return; |
||||
|
||||
AvailableVersions = new ObservableCollection<PackageVersion>(versions); |
||||
Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}"); |
||||
SelectedVersion = AvailableVersions[0]; |
||||
|
||||
if (!IsReleaseMode) |
||||
{ |
||||
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList(); |
||||
if (commits is null || commits.Count == 0) return; |
||||
|
||||
AvailableCommits = new ObservableCollection<GitCommit>(commits); |
||||
SelectedCommit = AvailableCommits[0]; |
||||
UpdateSelectedVersionToLatestMain(); |
||||
} |
||||
}).SafeFireAndForget(); |
||||
} |
||||
|
||||
private void UpdateSelectedVersionToLatestMain() |
||||
{ |
||||
if (AvailableVersions is null) |
||||
{ |
||||
SelectedVersion = null; |
||||
} |
||||
else |
||||
{ |
||||
// First try to find master |
||||
var version = AvailableVersions.FirstOrDefault(x => x.TagName == "master"); |
||||
// If not found, try main |
||||
version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main"); |
||||
|
||||
// If still not found, just use the first one |
||||
version ??= AvailableVersions[0]; |
||||
|
||||
SelectedVersion = version; |
||||
} |
||||
} |
||||
|
||||
public void AddPackageWithCurrentInputs() |
||||
{ |
||||
if (SelectedBasePackage is null || PackagePath is null) |
||||
return; |
||||
|
||||
string version; |
||||
if (IsReleaseMode) |
||||
{ |
||||
version = SelectedVersion?.TagName ?? |
||||
throw new NullReferenceException("Selected version is null"); |
||||
} |
||||
else |
||||
{ |
||||
version = SelectedCommit?.Sha ?? |
||||
throw new NullReferenceException("Selected commit is null"); |
||||
} |
||||
|
||||
var branch = SelectedVersionType == PackageVersionType.GithubRelease ? |
||||
null : SelectedVersion!.TagName; |
||||
|
||||
var package = new InstalledPackage |
||||
{ |
||||
Id = Guid.NewGuid(), |
||||
DisplayName = PackagePath.Name, |
||||
PackageName = SelectedBasePackage.Name, |
||||
LibraryPath = $"Packages{Path.DirectorySeparatorChar}{PackagePath.Name}", |
||||
PackageVersion = version, |
||||
DisplayVersion = GetDisplayVersion(version, branch), |
||||
InstalledBranch = branch, |
||||
LaunchCommand = SelectedBasePackage.LaunchCommand, |
||||
LastUpdateCheck = DateTimeOffset.Now, |
||||
}; |
||||
|
||||
settingsManager.Transaction(s => s.InstalledPackages.Add(package)); |
||||
} |
||||
} |
@ -0,0 +1,86 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
public class DownloadProgressItemViewModel : PausableProgressItemViewModelBase |
||||
{ |
||||
private readonly TrackedDownload download; |
||||
|
||||
public DownloadProgressItemViewModel(TrackedDownload download) |
||||
{ |
||||
this.download = download; |
||||
|
||||
Id = download.Id; |
||||
Name = download.FileName; |
||||
State = download.ProgressState; |
||||
OnProgressStateChanged(State); |
||||
|
||||
// If initial progress provided, load it |
||||
if (download is {TotalBytes: > 0, DownloadedBytes: > 0}) |
||||
{ |
||||
var current = download.DownloadedBytes / (double) download.TotalBytes; |
||||
Progress.Value = (float) Math.Ceiling(Math.Clamp(current, 0, 1) * 100); |
||||
} |
||||
|
||||
download.ProgressUpdate += (s, e) => |
||||
{ |
||||
Progress.Value = e.Percentage; |
||||
Progress.IsIndeterminate = e.IsIndeterminate; |
||||
}; |
||||
|
||||
download.ProgressStateChanged += (s, e) => |
||||
{ |
||||
State = e; |
||||
OnProgressStateChanged(e); |
||||
}; |
||||
} |
||||
|
||||
private void OnProgressStateChanged(ProgressState state) |
||||
{ |
||||
if (state == ProgressState.Inactive) |
||||
{ |
||||
Progress.Text = "Paused"; |
||||
} |
||||
else if (state == ProgressState.Working) |
||||
{ |
||||
Progress.Text = "Downloading..."; |
||||
} |
||||
else if (state == ProgressState.Success) |
||||
{ |
||||
Progress.Text = "Completed"; |
||||
} |
||||
else if (state == ProgressState.Cancelled) |
||||
{ |
||||
Progress.Text = "Cancelled"; |
||||
} |
||||
else if (state == ProgressState.Failed) |
||||
{ |
||||
Progress.Text = "Failed"; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Cancel() |
||||
{ |
||||
download.Cancel(); |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Pause() |
||||
{ |
||||
download.Pause(); |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task Resume() |
||||
{ |
||||
download.Resume(); |
||||
return Task.CompletedTask; |
||||
} |
||||
} |
@ -0,0 +1,221 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.Immutable; |
||||
using System.Collections.ObjectModel; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Net.Http; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Notifications; |
||||
using AvaloniaEdit.Utils; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using FluentAvalonia.UI.Controls; |
||||
using Microsoft.Extensions.Logging; |
||||
using Refit; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; |
||||
using StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Avalonia.Views.Dialogs; |
||||
using StabilityMatrix.Core.Api; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Database; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api; |
||||
using StabilityMatrix.Core.Services; |
||||
using Symbol = FluentIcons.Common.Symbol; |
||||
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
[View(typeof(NewCheckpointsPage))] |
||||
public partial class NewCheckpointsPageViewModel : PageViewModelBase |
||||
{ |
||||
private readonly ILogger<NewCheckpointsPageViewModel> logger; |
||||
private readonly ISettingsManager settingsManager; |
||||
private readonly ILiteDbContext liteDbContext; |
||||
private readonly ICivitApi civitApi; |
||||
private readonly ServiceManager<ViewModelBase> dialogFactory; |
||||
private readonly INotificationService notificationService; |
||||
public override string Title => "Checkpoint Manager"; |
||||
public override IconSource IconSource => new SymbolIconSource |
||||
{Symbol = Symbol.Cellular5g, IsFilled = true}; |
||||
|
||||
public NewCheckpointsPageViewModel(ILogger<NewCheckpointsPageViewModel> logger, |
||||
ISettingsManager settingsManager, ILiteDbContext liteDbContext, ICivitApi civitApi, |
||||
ServiceManager<ViewModelBase> dialogFactory, INotificationService notificationService) |
||||
{ |
||||
this.logger = logger; |
||||
this.settingsManager = settingsManager; |
||||
this.liteDbContext = liteDbContext; |
||||
this.civitApi = civitApi; |
||||
this.dialogFactory = dialogFactory; |
||||
this.notificationService = notificationService; |
||||
} |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(ConnectedCheckpoints))] |
||||
[NotifyPropertyChangedFor(nameof(NonConnectedCheckpoints))] |
||||
private ObservableCollection<CheckpointFile> allCheckpoints = new(); |
||||
|
||||
[ObservableProperty] |
||||
private ObservableCollection<CivitModel> civitModels = new(); |
||||
|
||||
public ObservableCollection<CheckpointFile> ConnectedCheckpoints => new( |
||||
AllCheckpoints.Where(x => x.IsConnectedModel) |
||||
.OrderBy(x => x.ConnectedModel!.ModelName) |
||||
.ThenBy(x => x.ModelType) |
||||
.GroupBy(x => x.ConnectedModel!.ModelId) |
||||
.Select(x => x.First())); |
||||
|
||||
public ObservableCollection<CheckpointFile> NonConnectedCheckpoints => new( |
||||
AllCheckpoints.Where(x => !x.IsConnectedModel).OrderBy(x => x.ModelType)); |
||||
|
||||
public override async Task OnLoadedAsync() |
||||
{ |
||||
if (Design.IsDesignMode) return; |
||||
|
||||
var files = CheckpointFile.GetAllCheckpointFiles(settingsManager.ModelsDirectory); |
||||
AllCheckpoints = new ObservableCollection<CheckpointFile>(files); |
||||
|
||||
var connectedModelIds = ConnectedCheckpoints.Select(x => x.ConnectedModel.ModelId); |
||||
var modelRequest = new CivitModelsRequest |
||||
{ |
||||
CommaSeparatedModelIds = string.Join(',', connectedModelIds) |
||||
}; |
||||
|
||||
// See if query is cached |
||||
var cachedQuery = await liteDbContext.CivitModelQueryCache |
||||
.IncludeAll() |
||||
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest)); |
||||
|
||||
// If cached, update model cards |
||||
if (cachedQuery is not null) |
||||
{ |
||||
CivitModels = new ObservableCollection<CivitModel>(cachedQuery.Items); |
||||
|
||||
// Start remote query (background mode) |
||||
// Skip when last query was less than 2 min ago |
||||
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt; |
||||
if (timeSinceCache?.TotalMinutes >= 2) |
||||
{ |
||||
CivitQuery(modelRequest).SafeFireAndForget(); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
await CivitQuery(modelRequest); |
||||
} |
||||
} |
||||
|
||||
public async Task ShowVersionDialog(int modelId) |
||||
{ |
||||
var model = CivitModels.FirstOrDefault(m => m.Id == modelId); |
||||
if (model == null) |
||||
{ |
||||
notificationService.Show(new Notification("Model has no versions available", |
||||
"This model has no versions available for download", NotificationType.Warning)); |
||||
return; |
||||
} |
||||
var versions = model.ModelVersions; |
||||
if (versions is null || versions.Count == 0) |
||||
{ |
||||
notificationService.Show(new Notification("Model has no versions available", |
||||
"This model has no versions available for download", NotificationType.Warning)); |
||||
return; |
||||
} |
||||
|
||||
var dialog = new BetterContentDialog |
||||
{ |
||||
Title = model.Name, |
||||
IsPrimaryButtonEnabled = false, |
||||
IsSecondaryButtonEnabled = false, |
||||
IsFooterVisible = false, |
||||
MaxDialogWidth = 750, |
||||
}; |
||||
|
||||
var viewModel = dialogFactory.Get<SelectModelVersionViewModel>(); |
||||
viewModel.Dialog = dialog; |
||||
viewModel.Versions = versions.Select(version => |
||||
new ModelVersionViewModel( |
||||
settingsManager.Settings.InstalledModelHashes ?? new HashSet<string>(), version)) |
||||
.ToImmutableArray(); |
||||
viewModel.SelectedVersionViewModel = viewModel.Versions[0]; |
||||
|
||||
dialog.Content = new SelectModelVersionDialog |
||||
{ |
||||
DataContext = viewModel |
||||
}; |
||||
|
||||
var result = await dialog.ShowAsync(); |
||||
|
||||
if (result != ContentDialogResult.Primary) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
var selectedVersion = viewModel?.SelectedVersionViewModel?.ModelVersion; |
||||
var selectedFile = viewModel?.SelectedFile?.CivitFile; |
||||
} |
||||
|
||||
private async Task CivitQuery(CivitModelsRequest request) |
||||
{ |
||||
try |
||||
{ |
||||
var modelResponse = await civitApi.GetModels(request); |
||||
var models = modelResponse.Items; |
||||
// Filter out unknown model types and archived/taken-down models |
||||
models = models.Where(m => m.Type.ConvertTo<SharedFolderType>() > 0) |
||||
.Where(m => m.Mode == null).ToList(); |
||||
|
||||
// Database update calls will invoke `OnModelsUpdated` |
||||
// Add to database |
||||
await liteDbContext.UpsertCivitModelAsync(models); |
||||
// Add as cache entry |
||||
var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync( |
||||
new CivitModelQueryCacheEntry |
||||
{ |
||||
Id = ObjectHash.GetMd5Guid(request), |
||||
InsertedAt = DateTimeOffset.UtcNow, |
||||
Request = request, |
||||
Items = models, |
||||
Metadata = modelResponse.Metadata |
||||
}); |
||||
|
||||
if (cacheNew) |
||||
{ |
||||
CivitModels = new ObservableCollection<CivitModel>(models); |
||||
} |
||||
} |
||||
catch (OperationCanceledException) |
||||
{ |
||||
notificationService.Show(new Notification("Request to CivitAI timed out", |
||||
"Could not check for checkpoint updates. Please try again later.")); |
||||
logger.LogWarning($"CivitAI query timed out ({request})"); |
||||
} |
||||
catch (HttpRequestException e) |
||||
{ |
||||
notificationService.Show(new Notification("CivitAI can't be reached right now", |
||||
"Could not check for checkpoint updates. Please try again later.")); |
||||
logger.LogWarning(e, $"CivitAI query HttpRequestException ({request})"); |
||||
} |
||||
catch (ApiException e) |
||||
{ |
||||
notificationService.Show(new Notification("CivitAI can't be reached right now", |
||||
"Could not check for checkpoint updates. Please try again later.")); |
||||
logger.LogWarning(e, $"CivitAI query ApiException ({request})"); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
notificationService.Show(new Notification("CivitAI can't be reached right now", |
||||
$"Unknown exception during CivitAI query: {e.GetType().Name}")); |
||||
logger.LogError(e, $"CivitAI query unknown exception ({request})"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,51 @@
|
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:ui="using:FluentAvalonia.UI.Controls" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:vmDialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
d:DataContext="{x:Static mocks:DesignData.PackageImportViewModel}" |
||||
x:DataType="vmDialogs:PackageImportViewModel" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PackageImportDialog"> |
||||
<StackPanel Margin="8" Spacing="8"> |
||||
<ui:SettingsExpander Header="{x:Static lang:Resources.Label_PackageType}"> |
||||
<ui:SettingsExpander.Footer> |
||||
<ui:FAComboBox |
||||
HorizontalAlignment="Stretch" |
||||
ItemsSource="{Binding AvailablePackages}" |
||||
DisplayMemberBinding="{Binding DisplayName}" |
||||
SelectedItem="{Binding SelectedBasePackage}"/> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
|
||||
<ui:SettingsExpander Header="{x:Static lang:Resources.Label_VersionType}"> |
||||
<ui:SettingsExpander.Footer> |
||||
<ToggleSwitch |
||||
IsEnabled="{Binding IsReleaseModeAvailable}" |
||||
OnContent="{x:Static lang:Resources.Label_Releases}" |
||||
OffContent="{x:Static lang:Resources.Label_Branches}" |
||||
IsChecked="{Binding IsReleaseMode}"/> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
|
||||
<ui:SettingsExpander Header="{x:Static lang:Resources.Label_Version}"> |
||||
<ui:SettingsExpander.Footer> |
||||
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
<ui:FAComboBox |
||||
ItemsSource="{Binding AvailableVersions}" |
||||
DisplayMemberBinding="{Binding TagName}" |
||||
SelectedItem="{Binding SelectedVersion}"/> |
||||
<ui:FAComboBox |
||||
IsVisible="{Binding !IsReleaseMode}" |
||||
ItemsSource="{Binding AvailableCommits}" |
||||
DisplayMemberBinding="{Binding Sha}" |
||||
SelectedItem="{Binding SelectedCommit}"/> |
||||
</StackPanel> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</StackPanel> |
||||
</controls:UserControlBase> |
@ -0,0 +1,17 @@
|
||||
using Avalonia.Markup.Xaml; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views.Dialogs; |
||||
|
||||
public partial class PackageImportDialog : UserControlBase |
||||
{ |
||||
public PackageImportDialog() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
|
||||
private void InitializeComponent() |
||||
{ |
||||
AvaloniaXamlLoader.Load(this); |
||||
} |
||||
} |
@ -0,0 +1,105 @@
|
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:checkpointManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
x:Class="StabilityMatrix.Avalonia.Views.NewCheckpointsPage" |
||||
d:DataContext="{x:Static mocks:DesignData.NewCheckpointsPageViewModel}" |
||||
x:CompileBindings="True" |
||||
x:Name="ControlBase" |
||||
x:DataType="viewModels:NewCheckpointsPageViewModel"> |
||||
<ScrollViewer> |
||||
<Grid RowDefinitions="Auto, Auto" Margin="8"> |
||||
<TextBlock Text="Installed Checkpoints" |
||||
FontSize="24" |
||||
Margin="8"/> |
||||
|
||||
<ItemsRepeater Grid.Row="1" ItemsSource="{Binding ConnectedCheckpoints}"> |
||||
<ItemsRepeater.Layout> |
||||
<UniformGridLayout /> |
||||
</ItemsRepeater.Layout> |
||||
<ItemsRepeater.ItemTemplate> |
||||
<DataTemplate DataType="{x:Type checkpointManager:CheckpointFile}"> |
||||
<controls:Card |
||||
Margin="8" |
||||
MaxHeight="450" |
||||
Width="300" |
||||
CornerRadius="8"> |
||||
<Grid RowDefinitions="Auto, Auto, Auto, Auto"> |
||||
<TextBlock Grid.Row="0" |
||||
Margin="0,0,0,8" |
||||
Text="{Binding ConnectedModel.ModelName}" /> |
||||
|
||||
<controls:BetterAdvancedImage |
||||
Grid.Row="1" |
||||
Height="250" |
||||
Stretch="UniformToFill" |
||||
CornerRadius="8" |
||||
Source="{Binding PreviewImagePath}" /> |
||||
|
||||
<Grid Grid.Row="1" |
||||
Margin="8,8,0,0" |
||||
ColumnDefinitions="Auto, Auto"> |
||||
<controls:Card |
||||
Grid.Column="0" |
||||
Classes="info" |
||||
Height="24" |
||||
HorizontalAlignment="Left" |
||||
Padding="4" |
||||
Margin="0,0,4,0" |
||||
VerticalAlignment="Top"> |
||||
|
||||
<TextBlock |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
HorizontalAlignment="Center" |
||||
Text="{Binding ModelType}" |
||||
VerticalAlignment="Center" /> |
||||
</controls:Card> |
||||
<controls:Card |
||||
Grid.Column="1" |
||||
Classes="info" |
||||
Height="24" |
||||
Margin="4,0" |
||||
HorizontalAlignment="Left" |
||||
Padding="4" |
||||
VerticalAlignment="Top"> |
||||
|
||||
<TextBlock |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
HorizontalAlignment="Center" |
||||
Text="{Binding ConnectedModel.BaseModel}" |
||||
VerticalAlignment="Center" /> |
||||
</controls:Card> |
||||
</Grid> |
||||
|
||||
<Grid Grid.Row="2" |
||||
Margin="0,16,0,0" |
||||
ColumnDefinitions="*, *"> |
||||
<Button Grid.Column="0" |
||||
Content="Update" |
||||
Classes="accent" |
||||
Margin="0,0,4,0" |
||||
HorizontalAlignment="Stretch" /> |
||||
<Button Grid.Column="1" |
||||
Margin="4,0,0,0" |
||||
Content="All Versions" |
||||
Classes="accent" |
||||
Command="{Binding $parent[ItemsRepeater].((viewModels:NewCheckpointsPageViewModel)DataContext).ShowVersionDialog}" |
||||
CommandParameter="{Binding ConnectedModel.ModelId}" |
||||
HorizontalAlignment="Stretch" /> |
||||
</Grid> |
||||
|
||||
</Grid> |
||||
</controls:Card> |
||||
</DataTemplate> |
||||
</ItemsRepeater.ItemTemplate> |
||||
</ItemsRepeater> |
||||
</Grid> |
||||
</ScrollViewer> |
||||
</controls:UserControlBase> |
@ -0,0 +1,11 @@
|
||||
using StabilityMatrix.Avalonia.Controls; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
public partial class NewCheckpointsPage : UserControlBase |
||||
{ |
||||
public NewCheckpointsPage() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
@ -1,317 +1,368 @@
|
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:ui="using:FluentAvalonia.UI.Controls" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700" |
||||
x:DataType="vm:SettingsViewModel" |
||||
x:CompileBindings="True" |
||||
d:DataContext="{x:Static mocks:DesignData.SettingsViewModel}" |
||||
x:Class="StabilityMatrix.Avalonia.Views.SettingsPage"> |
||||
|
||||
<Grid> |
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"> |
||||
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto" |
||||
Margin="8, 16"> |
||||
<!-- Theme --> |
||||
<Grid Grid.Row="0" RowDefinitions="auto,*"> |
||||
<TextBlock |
||||
FontWeight="Medium" |
||||
Text="Appearance" |
||||
Margin="0,0,0,8" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
Header="Theme" |
||||
IconSource="WeatherMoon" |
||||
Margin="8,0,8,4"> |
||||
<ui:SettingsExpander.Footer> |
||||
<ComboBox |
||||
ItemsSource="{Binding AvailableThemes}" |
||||
SelectedItem="{Binding SelectedTheme}" |
||||
MinWidth="100"/> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- TODO: Text2Image host port settings --> |
||||
|
||||
<!-- Checkpoints Manager Options --> |
||||
<Grid Grid.Row="1" Margin="0,8,0,0" RowDefinitions="auto,*,Auto"> |
||||
<TextBlock |
||||
FontWeight="Medium" |
||||
Text="Checkpoint Manager" |
||||
Margin="0,0,0,8" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
IconSource="Folder" |
||||
Header="Remove shared checkpoints directory symbolic links on shutdown" |
||||
Description="Select this option if you're having problems moving Stability Matrix to another drive" |
||||
Margin="8,0"> |
||||
<ui:SettingsExpander.Footer> |
||||
<CheckBox Margin="8" |
||||
IsChecked="{Binding RemoveSymlinksOnShutdown}"/> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
<ui:SettingsExpander |
||||
Grid.Row="2" |
||||
IconSource="Refresh" |
||||
Header="Reset Checkpoints Cache" |
||||
Description="Rebuilds the installed checkpoints cache. Use if checkpoints are incorrectly labeled in the Model Browser." |
||||
Margin="8, 4"> |
||||
<ui:SettingsExpander.Footer> |
||||
<Button Command="{Binding ResetCheckpointCache}" |
||||
Content="Reset Checkpoints Cache"/> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- Environment Options --> |
||||
<Grid Grid.Row="2" Margin="0,8,0,0" RowDefinitions="Auto, Auto, Auto"> |
||||
<TextBlock |
||||
FontWeight="Medium" |
||||
Text="Package Environment" |
||||
Margin="0,0,0,8" /> |
||||
|
||||
<ui:SettingsExpander Grid.Row="1" |
||||
Header="Environment Variables" |
||||
IconSource="OtherUser" |
||||
Margin="8,0"> |
||||
<ui:SettingsExpander.Footer> |
||||
<Button Content="Edit" |
||||
Command="{Binding OpenEnvVarsDialogCommand}"/> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
|
||||
<ui:SettingsExpander Grid.Row="2" |
||||
Header="Embedded Python" |
||||
Margin="8,4"> |
||||
<ui:SettingsExpander.IconSource> |
||||
<controls:FASymbolIconSource Symbol="fa-brands fa-python"/> |
||||
</ui:SettingsExpander.IconSource> |
||||
<ui:SettingsExpander.Footer> |
||||
<StackPanel Orientation="Horizontal" Spacing="16"> |
||||
<controls:ProgressRing |
||||
IsEnabled="{Binding CheckPythonVersionCommand.IsRunning}" |
||||
IsVisible="{Binding CheckPythonVersionCommand.IsRunning}" |
||||
IsIndeterminate="True" |
||||
BorderThickness="3"/> |
||||
<Button Content="Check Version" Command="{Binding CheckPythonVersionCommand}"/> |
||||
</StackPanel> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- Integrations --> |
||||
<Grid Grid.Row="3" Margin="0,8,0,0" RowDefinitions="auto,*"> |
||||
<TextBlock |
||||
FontWeight="Medium" |
||||
Text="Integrations" |
||||
Margin="0,0,0,8" /> |
||||
<ui:SettingsExpander Grid.Row="1" |
||||
Header="Discord Rich Presence" |
||||
Margin="8,0,8,4"> |
||||
<ui:SettingsExpander.IconSource> |
||||
<controls:FASymbolIconSource Symbol="fa-brands fa-discord"/> |
||||
</ui:SettingsExpander.IconSource> |
||||
<ui:SettingsExpander.Footer> |
||||
<ToggleSwitch |
||||
IsChecked="{Binding IsDiscordRichPresenceEnabled}" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- System Options --> |
||||
<Grid Grid.Row="4" Margin="0,8,0,0" RowDefinitions="auto,*"> |
||||
<TextBlock |
||||
FontWeight="Medium" |
||||
Text="System" |
||||
Margin="0,0,0,8" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
ToolTip.Tip="{OnPlatform Default='Only available on Windows', Windows={x:Null}}" |
||||
Header="Add Stability Matrix to the Start Menu" |
||||
Description="Uses the current app location, you can run this again if you move the app" |
||||
IconSource="StarAdd" |
||||
Margin="8,0,8,4"> |
||||
<ui:SettingsExpander.Footer> |
||||
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
<controls:ProgressRing |
||||
IsIndeterminate="True" |
||||
IsEnabled="{Binding IsVisible, RelativeSource={RelativeSource Self}}" |
||||
BorderThickness="3"> |
||||
<controls:ProgressRing.IsVisible> |
||||
<MultiBinding Converter="{x:Static BoolConverters.Or}"> |
||||
<Binding Path="AddToStartMenuCommand.IsRunning"/> |
||||
<Binding Path="AddToGlobalStartMenuCommand.IsRunning"/> |
||||
</MultiBinding> |
||||
</controls:ProgressRing.IsVisible> |
||||
</controls:ProgressRing> |
||||
|
||||
<SplitButton |
||||
Command="{Binding AddToStartMenuCommand}" |
||||
IsEnabled="{OnPlatform Default=False, Windows=True}" |
||||
Content="Add for Current User"> |
||||
<SplitButton.Flyout> |
||||
<ui:FAMenuFlyout Placement="Bottom"> |
||||
<ui:MenuFlyoutItem |
||||
Command="{Binding AddToGlobalStartMenuCommand}" |
||||
IconSource="Admin" |
||||
Text="Add for All Users"/> |
||||
</ui:FAMenuFlyout> |
||||
</SplitButton.Flyout> |
||||
</SplitButton> |
||||
</StackPanel> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- Debug Options --> |
||||
<Grid Grid.Row="5" RowDefinitions="auto,*" |
||||
Margin="0,8,0,0" |
||||
IsVisible="{Binding SharedState.IsDebugMode}" > |
||||
<TextBlock |
||||
FontWeight="Medium" |
||||
Text="Debug Options" |
||||
Margin="0,0,0,8" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
IconSource="Code" |
||||
Command="{Binding LoadDebugInfo}" |
||||
Header="Debug Options" |
||||
Margin="8, 0,8,4"> |
||||
|
||||
<ui:SettingsExpanderItem Description="Paths" IconSource="Folder" |
||||
Margin="4"> |
||||
<SelectableTextBlock Text="{Binding DebugPaths}" |
||||
Foreground="{DynamicResource TextControlPlaceholderForeground}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
</ui:SettingsExpanderItem> |
||||
<controls:UserControlBase |
||||
x:Class="StabilityMatrix.Avalonia.Views.SettingsPage" |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:ui="using:FluentAvalonia.UI.Controls" |
||||
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
d:DataContext="{x:Static mocks:DesignData.SettingsViewModel}" |
||||
d:DesignHeight="700" |
||||
d:DesignWidth="800" |
||||
x:CompileBindings="True" |
||||
x:DataType="vm:SettingsViewModel" |
||||
mc:Ignorable="d"> |
||||
|
||||
<ui:SettingsExpanderItem Description="Compat Info" IconSource="StarFilled" |
||||
Margin="4,0,4,4"> |
||||
<SelectableTextBlock Text="{Binding DebugCompatInfo}" |
||||
Foreground="{DynamicResource TextControlPlaceholderForeground}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem Description="GPU Info" IconSource="FullScreenMaximize" |
||||
Margin="4,0,4,4"> |
||||
<SelectableTextBlock Text="{Binding DebugGpuInfo}" |
||||
Foreground="{DynamicResource TextControlPlaceholderForeground}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
</ui:SettingsExpanderItem> |
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"> |
||||
<StackPanel Margin="8,16" Spacing="8"> |
||||
<!-- Theme --> |
||||
<Grid RowDefinitions="Auto,*,*"> |
||||
<TextBlock |
||||
Margin="0,0,0,8" |
||||
FontWeight="Medium" |
||||
Text="Appearance" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
Margin="8,0,8,4" |
||||
Header="Theme" |
||||
IconSource="WeatherMoon"> |
||||
<ui:SettingsExpander.Footer> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AvailableThemes}" |
||||
SelectedItem="{Binding SelectedTheme}" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
<ui:SettingsExpander |
||||
Grid.Row="2" |
||||
IsVisible="{Binding SharedState.IsDebugMode}" |
||||
Margin="8,0,8,4" |
||||
Header="{x:Static lang:Resources.Label_Language}" |
||||
IconSource="Character"> |
||||
<ui:SettingsExpander.Footer> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AvailableLanguages}" |
||||
DisplayMemberBinding="{Binding NativeName}" |
||||
SelectedItem="{Binding SelectedLanguage}" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<ui:SettingsExpanderItem Content="Animation Scale" IconSource="Clock" |
||||
Description="Lower values = faster animations. 0x means animations are instant." |
||||
Margin="4,0,4,4"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<ComboBox Margin="0, 8" |
||||
ItemsSource="{Binding AnimationScaleOptions}" |
||||
SelectedItem="{Binding SelectedAnimationScale}"> |
||||
<ComboBox.ItemTemplate> |
||||
<DataTemplate> |
||||
<TextBlock> |
||||
<Run Text="{Binding }"/><Run Text="x"/> |
||||
</TextBlock> |
||||
</DataTemplate> |
||||
</ComboBox.ItemTemplate> |
||||
</ComboBox> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem Content="Notification" IconSource="CommentAdd" |
||||
Margin="4,0,4,4"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<Button |
||||
Margin="0, 8" |
||||
Command="{Binding DebugNotificationCommand}" |
||||
Content="New Notification"/> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem Content="Content Dialog" IconSource="NewWindow" |
||||
Margin="4,0,4,4"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<Button |
||||
Margin="0, 8" |
||||
Command="{Binding DebugContentDialogCommand}" |
||||
Content="Show Dialog"/> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem Content="Exceptions" IconSource="Flag" |
||||
Margin="4,0,4,4"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<Button |
||||
Margin="0, 8" |
||||
Command="{Binding DebugThrowExceptionCommand}" |
||||
Content="Unhandled Exception"/> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- TODO: Python card --> |
||||
|
||||
<!-- TODO: Debug card --> |
||||
|
||||
<!-- TODO: Directories card --> |
||||
|
||||
<Grid Grid.Row="6" RowDefinitions="auto,*" Margin="0,4,0,0"> |
||||
<StackPanel |
||||
Grid.Row="1" |
||||
HorizontalAlignment="Left" |
||||
Orientation="Vertical"> |
||||
<TextBlock |
||||
FontSize="15" |
||||
FontWeight="Bold" |
||||
Margin="0,8" |
||||
Text="About" /> |
||||
<Image |
||||
Height="112" |
||||
HorizontalAlignment="Left" |
||||
Margin="8" |
||||
Source="/Assets/Icon.png" |
||||
Width="112" /> |
||||
<TextBlock |
||||
FontWeight="Medium" |
||||
Margin="8" |
||||
Text="Stability Matrix" /> |
||||
<Panel> |
||||
<Button |
||||
Name="VersionButton" |
||||
Command="{Binding OnVersionClick}" |
||||
Classes="transparent" |
||||
BorderThickness="0" |
||||
Content="{Binding AppVersion}" |
||||
Margin="8,0,8,8" |
||||
Padding="2,0,2,0"/> |
||||
<ui:TeachingTip |
||||
PreferredPlacement="RightTop" |
||||
Target="{Binding #VersionButton}" |
||||
IsOpen="{Binding IsVersionTapTeachingTipOpen}" |
||||
Title="{Binding VersionFlyoutText}"/> |
||||
</Panel> |
||||
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> |
||||
<Button |
||||
Content="License and Open Source Notices" |
||||
Command="{Binding ShowLicensesDialogCommand}" |
||||
HorizontalAlignment="Left" |
||||
Margin="8" /> |
||||
</StackPanel> |
||||
<!-- Checkpoints Manager Options --> |
||||
<Grid RowDefinitions="auto,*,Auto"> |
||||
<TextBlock |
||||
Margin="0,0,0,8" |
||||
FontWeight="Medium" |
||||
Text="Checkpoint Manager" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
Margin="8,0" |
||||
Description="Select this option if you're having problems moving Stability Matrix to another drive" |
||||
Header="Remove shared checkpoints directory symbolic links on shutdown" |
||||
IconSource="Folder"> |
||||
<ui:SettingsExpander.Footer> |
||||
<CheckBox Margin="8" IsChecked="{Binding RemoveSymlinksOnShutdown}" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
<ui:SettingsExpander |
||||
Grid.Row="2" |
||||
Margin="8,4" |
||||
Description="Rebuilds the installed checkpoints cache. Use if checkpoints are incorrectly labeled in the Model Browser." |
||||
Header="Reset Checkpoints Cache" |
||||
IconSource="Refresh"> |
||||
<ui:SettingsExpander.Footer> |
||||
<Button Command="{Binding ResetCheckpointCache}" Content="Reset Checkpoints Cache" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- Environment Options --> |
||||
<Grid RowDefinitions="Auto, Auto, Auto"> |
||||
<TextBlock |
||||
Margin="0,0,0,8" |
||||
FontWeight="Medium" |
||||
Text="Package Environment" /> |
||||
|
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
Margin="8,0" |
||||
Header="Environment Variables" |
||||
IconSource="OtherUser"> |
||||
<ui:SettingsExpander.Footer> |
||||
<Button Command="{Binding OpenEnvVarsDialogCommand}" Content="Edit" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
|
||||
<ui:SettingsExpander |
||||
Grid.Row="2" |
||||
Margin="8,4" |
||||
Header="Embedded Python"> |
||||
<ui:SettingsExpander.IconSource> |
||||
<controls:FASymbolIconSource Symbol="fa-brands fa-python" /> |
||||
</ui:SettingsExpander.IconSource> |
||||
<ui:SettingsExpander.Footer> |
||||
<StackPanel Orientation="Horizontal" Spacing="16"> |
||||
<controls:ProgressRing |
||||
BorderThickness="3" |
||||
IsEnabled="{Binding CheckPythonVersionCommand.IsRunning}" |
||||
IsIndeterminate="True" |
||||
IsVisible="{Binding CheckPythonVersionCommand.IsRunning}" /> |
||||
<Button Command="{Binding CheckPythonVersionCommand}" Content="Check Version" /> |
||||
</StackPanel> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- Integrations --> |
||||
<Grid RowDefinitions="auto,*"> |
||||
<TextBlock |
||||
Margin="0,0,0,8" |
||||
FontWeight="Medium" |
||||
Text="Integrations" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
Margin="8,0,8,4" |
||||
Header="Discord Rich Presence"> |
||||
<ui:SettingsExpander.IconSource> |
||||
<controls:FASymbolIconSource Symbol="fa-brands fa-discord" /> |
||||
</ui:SettingsExpander.IconSource> |
||||
<ui:SettingsExpander.Footer> |
||||
<ToggleSwitch IsChecked="{Binding IsDiscordRichPresenceEnabled}" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- System Options --> |
||||
<Grid RowDefinitions="auto, auto, auto"> |
||||
<TextBlock |
||||
Margin="0,0,0,8" |
||||
FontWeight="Medium" |
||||
Text="System" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
Margin="8,0,8,4" |
||||
Description="Uses the current app location, you can run this again if you move the app" |
||||
Header="Add Stability Matrix to the Start Menu" |
||||
IconSource="StarAdd" |
||||
ToolTip.Tip="{OnPlatform Default='Only available on Windows', |
||||
Windows={x:Null}}"> |
||||
<ui:SettingsExpander.Footer> |
||||
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
<controls:ProgressRing |
||||
BorderThickness="3" |
||||
IsEnabled="{Binding IsVisible, RelativeSource={RelativeSource Self}}" |
||||
IsIndeterminate="True"> |
||||
<controls:ProgressRing.IsVisible> |
||||
<MultiBinding Converter="{x:Static BoolConverters.Or}"> |
||||
<Binding Path="AddToStartMenuCommand.IsRunning" /> |
||||
<Binding Path="AddToGlobalStartMenuCommand.IsRunning" /> |
||||
</MultiBinding> |
||||
</controls:ProgressRing.IsVisible> |
||||
</controls:ProgressRing> |
||||
|
||||
<SplitButton |
||||
Command="{Binding AddToStartMenuCommand}" |
||||
Content="Add for Current User" |
||||
IsEnabled="{OnPlatform Default=False, |
||||
Windows=True}"> |
||||
<SplitButton.Flyout> |
||||
<ui:FAMenuFlyout Placement="Bottom"> |
||||
<ui:MenuFlyoutItem |
||||
Command="{Binding AddToGlobalStartMenuCommand}" |
||||
IconSource="Admin" |
||||
Text="Add for All Users" /> |
||||
</ui:FAMenuFlyout> |
||||
</SplitButton.Flyout> |
||||
</SplitButton> |
||||
</StackPanel> |
||||
</Grid> |
||||
|
||||
<!-- Extra space at the bottom --> |
||||
<Panel Grid.Row="7" Margin="0,0,0,16" /> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
<ui:SettingsExpander |
||||
Grid.Row="2" |
||||
Margin="8,0" |
||||
Description="Does not move existing data" |
||||
Header="Select New Data Directory" |
||||
IconSource="MoveToFolder"> |
||||
<ui:SettingsExpander.Footer> |
||||
<Button Command="{Binding PickNewDataDirectory}"> |
||||
<Grid ColumnDefinitions="Auto, Auto"> |
||||
<avalonia:Icon |
||||
Grid.Row="0" |
||||
Margin="0,0,8,0" |
||||
VerticalAlignment="Center" |
||||
Value="fa-solid fa-folder-open" /> |
||||
<TextBlock |
||||
Grid.Column="1" |
||||
VerticalAlignment="Center" |
||||
Text="Select Directory" /> |
||||
</Grid> |
||||
</Button> |
||||
</ui:SettingsExpander.Footer> |
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
</ScrollViewer> |
||||
</Grid> |
||||
|
||||
|
||||
|
||||
<!-- Debug Options --> |
||||
<Grid |
||||
IsVisible="{Binding SharedState.IsDebugMode}" |
||||
RowDefinitions="auto,*"> |
||||
<TextBlock |
||||
Margin="0,0,0,8" |
||||
FontWeight="Medium" |
||||
Text="Debug Options" /> |
||||
<ui:SettingsExpander |
||||
Grid.Row="1" |
||||
Margin="8,0,8,0" |
||||
Command="{Binding LoadDebugInfo}" |
||||
Header="Debug Options" |
||||
IconSource="Code"> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0" |
||||
Description="Paths" |
||||
IconSource="Folder"> |
||||
<SelectableTextBlock |
||||
Foreground="{DynamicResource TextControlPlaceholderForeground}" |
||||
Text="{Binding DebugPaths}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0" |
||||
Description="Compat Info" |
||||
IconSource="StarFilled"> |
||||
<SelectableTextBlock |
||||
Foreground="{DynamicResource TextControlPlaceholderForeground}" |
||||
Text="{Binding DebugCompatInfo}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0" |
||||
Description="GPU Info" |
||||
IconSource="FullScreenMaximize"> |
||||
<SelectableTextBlock |
||||
Foreground="{DynamicResource TextControlPlaceholderForeground}" |
||||
Text="{Binding DebugGpuInfo}" |
||||
TextWrapping="WrapWithOverflow" /> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0" |
||||
Content="Animation Scale" |
||||
Description="Lower values = faster animations. 0x means animations are instant." |
||||
IconSource="Clock"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<ComboBox ItemsSource="{Binding AnimationScaleOptions}" SelectedItem="{Binding SelectedAnimationScale}"> |
||||
<ComboBox.ItemTemplate> |
||||
<DataTemplate> |
||||
<TextBlock> |
||||
<Run Text="{Binding}" /><Run Text="x" /> |
||||
</TextBlock> |
||||
</DataTemplate> |
||||
</ComboBox.ItemTemplate> |
||||
</ComboBox> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0" |
||||
Content="Notification" |
||||
IconSource="CommentAdd"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<Button Command="{Binding DebugNotificationCommand}" Content="New Notification" /> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0" |
||||
Content="Content Dialog" |
||||
IconSource="NewWindow"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<Button Command="{Binding DebugContentDialogCommand}" Content="Show Dialog" /> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0" |
||||
Content="Exceptions" |
||||
IconSource="Flag"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<Button Command="{Binding DebugThrowExceptionCommand}" Content="Unhandled Exception" /> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
<ui:SettingsExpanderItem |
||||
Margin="4,0,4,4" |
||||
Content="Download Manager tests" |
||||
IconSource="Flag"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<Button |
||||
Margin="0,8" |
||||
Command="{Binding DebugTrackedDownloadCommand}" |
||||
Content="Add Tracked Download" /> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
|
||||
</ui:SettingsExpander> |
||||
</Grid> |
||||
|
||||
<!-- TODO: Directories card --> |
||||
|
||||
<Grid RowDefinitions="auto,*"> |
||||
<StackPanel |
||||
Grid.Row="1" |
||||
HorizontalAlignment="Left" |
||||
Orientation="Vertical"> |
||||
<TextBlock |
||||
Margin="0,8" |
||||
FontSize="15" |
||||
FontWeight="Bold" |
||||
Text="About" /> |
||||
<Image |
||||
Width="112" |
||||
Height="112" |
||||
Margin="8" |
||||
HorizontalAlignment="Left" |
||||
Source="/Assets/Icon.png" /> |
||||
<TextBlock |
||||
Margin="8" |
||||
FontWeight="Medium" |
||||
Text="Stability Matrix" /> |
||||
<Panel> |
||||
<Button |
||||
Name="VersionButton" |
||||
Margin="8,0,8,8" |
||||
Padding="2,0,2,0" |
||||
BorderThickness="0" |
||||
Classes="transparent" |
||||
Command="{Binding OnVersionClick}" |
||||
Content="{Binding AppVersion}" /> |
||||
<ui:TeachingTip |
||||
Title="{Binding VersionFlyoutText}" |
||||
IsOpen="{Binding IsVersionTapTeachingTipOpen}" |
||||
PreferredPlacement="RightTop" |
||||
Target="{Binding #VersionButton}" /> |
||||
</Panel> |
||||
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> |
||||
<Button |
||||
Margin="8" |
||||
HorizontalAlignment="Left" |
||||
Command="{Binding ShowLicensesDialogCommand}" |
||||
Content="License and Open Source Notices" /> |
||||
</StackPanel> |
||||
</StackPanel> |
||||
</Grid> |
||||
|
||||
<!-- Extra space at the bottom --> |
||||
<Panel Margin="0,0,0,16" /> |
||||
</StackPanel> |
||||
</ScrollViewer> |
||||
|
||||
|
||||
</controls:UserControlBase> |
||||
|
@ -0,0 +1,34 @@
|
||||
using System.Text.Json; |
||||
using System.Text.Json.Serialization; |
||||
|
||||
namespace StabilityMatrix.Core.Converters.Json; |
||||
|
||||
/// <summary> |
||||
/// Json converter for types that serialize to string by `ToString()` and |
||||
/// can be created by `Activator.CreateInstance(Type, string)` |
||||
/// </summary> |
||||
public class StringJsonConverter<T> : JsonConverter<T> |
||||
{ |
||||
/// <inheritdoc /> |
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) |
||||
{ |
||||
if (reader.TokenType != JsonTokenType.String) |
||||
{ |
||||
throw new JsonException(); |
||||
} |
||||
|
||||
var value = reader.GetString(); |
||||
if (value is null) |
||||
{ |
||||
throw new JsonException(); |
||||
} |
||||
|
||||
return (T?) Activator.CreateInstance(typeToConvert, value); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) |
||||
{ |
||||
writer.WriteStringValue(value?.ToString()); |
||||
} |
||||
} |
@ -0,0 +1,24 @@
|
||||
namespace StabilityMatrix.Core.Helper; |
||||
|
||||
public class PropertyComparer<T> : IEqualityComparer<T> where T : class
|
||||
{ |
||||
private Func<T, object> Expr { get; set; } |
||||
|
||||
public PropertyComparer(Func<T, object> expr) |
||||
{ |
||||
Expr = expr; |
||||
} |
||||
public bool Equals(T? x, T? y) |
||||
{ |
||||
if (x == null || y == null) return false; |
||||
|
||||
var first = Expr.Invoke(x); |
||||
var second = Expr.Invoke(y); |
||||
|
||||
return first.Equals(second); |
||||
} |
||||
public int GetHashCode(T obj) |
||||
{ |
||||
return obj.GetHashCode(); |
||||
} |
||||
} |
@ -0,0 +1,44 @@
|
||||
using System.Diagnostics; |
||||
using System.Text.Json; |
||||
using StabilityMatrix.Core.Models.Api; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Core.Models; |
||||
|
||||
public class CivitPostDownloadContextAction : IContextAction |
||||
{ |
||||
/// <inheritdoc /> |
||||
public object? Context { get; set; } |
||||
|
||||
public static CivitPostDownloadContextAction FromCivitFile(CivitFile file) |
||||
{ |
||||
return new CivitPostDownloadContextAction |
||||
{ |
||||
Context = file.Hashes.BLAKE3 |
||||
}; |
||||
} |
||||
|
||||
public void Invoke(ISettingsManager settingsManager) |
||||
{ |
||||
var result = Context as string; |
||||
|
||||
if (Context is JsonElement jsonElement) |
||||
{ |
||||
result = jsonElement.GetString(); |
||||
} |
||||
|
||||
if (result is null) |
||||
{ |
||||
Debug.WriteLine($"Context {Context} is not a string."); |
||||
return; |
||||
} |
||||
|
||||
Debug.WriteLine($"Adding {result} to installed models."); |
||||
settingsManager.Transaction( |
||||
s => |
||||
{ |
||||
s.InstalledModelHashes ??= new HashSet<string>(); |
||||
s.InstalledModelHashes.Add(result); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization; |
||||
|
||||
namespace StabilityMatrix.Core.Models; |
||||
|
||||
[JsonDerivedType(typeof(CivitPostDownloadContextAction), "CivitPostDownload")] |
||||
public interface IContextAction |
||||
{ |
||||
object? Context { get; set; } |
||||
} |
@ -0,0 +1,122 @@
|
||||
using System.Diagnostics; |
||||
using System.Text.RegularExpressions; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Helper.Cache; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
using StabilityMatrix.Core.Processes; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Core.Models.Packages; |
||||
|
||||
public class Fooocus : BaseGitPackage |
||||
{ |
||||
public Fooocus(IGithubApiCache githubApi, ISettingsManager settingsManager, |
||||
IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper) : base(githubApi, |
||||
settingsManager, downloadService, prerequisiteHelper) |
||||
{ |
||||
} |
||||
|
||||
public override string Name => "Fooocus"; |
||||
public override string DisplayName { get; set; } = "Fooocus"; |
||||
public override string Author => "lllyasviel"; |
||||
|
||||
public override string Blurb => |
||||
"Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs"; |
||||
|
||||
public override string LicenseType => "GPL-3.0"; |
||||
public override string LicenseUrl => "https://github.com/lllyasviel/Fooocus/blob/main/LICENSE"; |
||||
public override string LaunchCommand => "launch.py"; |
||||
|
||||
public override Uri PreviewImageUri => |
||||
new("https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png"); |
||||
|
||||
public override List<LaunchOptionDefinition> LaunchOptions => new() |
||||
{ |
||||
LaunchOptionDefinition.Extras |
||||
}; |
||||
|
||||
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() |
||||
{ |
||||
[SharedFolderType.StableDiffusion] = new[] {"models/checkpoints"}, |
||||
[SharedFolderType.Diffusers] = new[] {"models/diffusers"}, |
||||
[SharedFolderType.Lora] = new[] {"models/loras"}, |
||||
[SharedFolderType.CLIP] = new[] {"models/clip"}, |
||||
[SharedFolderType.TextualInversion] = new[] {"models/embeddings"}, |
||||
[SharedFolderType.VAE] = new[] {"models/vae"}, |
||||
[SharedFolderType.ApproxVAE] = new[] {"models/vae_approx"}, |
||||
[SharedFolderType.ControlNet] = new[] {"models/controlnet"}, |
||||
[SharedFolderType.GLIGEN] = new[] {"models/gligen"}, |
||||
[SharedFolderType.ESRGAN] = new[] {"models/upscale_models"}, |
||||
[SharedFolderType.Hypernetwork] = new[] {"models/hypernetworks"} |
||||
}; |
||||
|
||||
public override async Task<string> GetLatestVersion() |
||||
{ |
||||
var release = await GetLatestRelease().ConfigureAwait(false); |
||||
return release.TagName!; |
||||
} |
||||
|
||||
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) |
||||
{ |
||||
await base.InstallPackage(progress).ConfigureAwait(false); |
||||
var venvRunner = await SetupVenv(InstallLocation).ConfigureAwait(false); |
||||
|
||||
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); |
||||
|
||||
var torchVersion = "cpu"; |
||||
var gpus = HardwareHelper.IterGpuInfo().ToList(); |
||||
|
||||
if (gpus.Any(g => g.IsNvidia)) |
||||
{ |
||||
torchVersion = "cu118"; |
||||
} |
||||
else if (HardwareHelper.PreferRocm()) |
||||
{ |
||||
torchVersion = "rocm5.4.2"; |
||||
} |
||||
|
||||
await venvRunner |
||||
.PipInstall( |
||||
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersion}", |
||||
OnConsoleOutput).ConfigureAwait(false); |
||||
|
||||
progress?.Report(new ProgressReport(-1f, "Installing requirements...", |
||||
isIndeterminate: true)); |
||||
await venvRunner.PipInstall("-r requirements_versions.txt", OnConsoleOutput) |
||||
.ConfigureAwait(false); |
||||
} |
||||
|
||||
public override async Task RunPackage(string installedPackagePath, string command, string arguments) |
||||
{ |
||||
await SetupVenv(installedPackagePath).ConfigureAwait(false); |
||||
|
||||
void HandleConsoleOutput(ProcessOutput s) |
||||
{ |
||||
OnConsoleOutput(s); |
||||
|
||||
if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase)) |
||||
{ |
||||
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); |
||||
var match = regex.Match(s.Text); |
||||
if (match.Success) |
||||
{ |
||||
WebUrl = match.Value; |
||||
} |
||||
OnStartupComplete(WebUrl); |
||||
} |
||||
} |
||||
|
||||
void HandleExit(int i) |
||||
{ |
||||
Debug.WriteLine($"Venv process exited with code {i}"); |
||||
OnExit(i); |
||||
} |
||||
|
||||
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}"; |
||||
|
||||
VenvRunner?.RunDetached( |
||||
args.TrimEnd(), |
||||
HandleConsoleOutput, |
||||
HandleExit); |
||||
} |
||||
} |
@ -0,0 +1,105 @@
|
||||
using Octokit; |
||||
using StabilityMatrix.Core.Models.Database; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Core.Models.Packages; |
||||
|
||||
public class UnknownPackage : BasePackage |
||||
{ |
||||
public static string Key => "unknown-package"; |
||||
public override string Name => Key; |
||||
public override string DisplayName { get; set; } = "Unknown Package"; |
||||
public override string Author => ""; |
||||
|
||||
public override string GithubUrl => ""; |
||||
public override string LicenseType => "AGPL-3.0"; |
||||
public override string LicenseUrl => |
||||
"https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE"; |
||||
public override string Blurb => "A dank interface for diffusion"; |
||||
public override string LaunchCommand => "test"; |
||||
|
||||
public override Uri PreviewImageUri => new(""); |
||||
|
||||
public override IReadOnlyList<string> ExtraLaunchCommands => new[] |
||||
{ |
||||
"test-config", |
||||
}; |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task<string> DownloadPackage(string version, bool isCommitHash, string? branch, IProgress<ProgressReport>? progress = null) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task InstallPackage(IProgress<ProgressReport>? progress = null) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
public override Task RunPackage(string installedPackagePath, string command, string arguments) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task SetupModelFolders(DirectoryPath installDirectory) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task UpdateModelFolders(DirectoryPath installDirectory) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override void Shutdown() |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task WaitForShutdown() |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task<bool> CheckForUpdates(InstalledPackage package) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task<string> Update(InstalledPackage installedPackage, IProgress<ProgressReport>? progress = null, |
||||
bool includePrerelease = false) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task<IEnumerable<Release>> GetReleaseTags() => Task.FromResult(Enumerable.Empty<Release>()); |
||||
|
||||
public override List<LaunchOptionDefinition> LaunchOptions => new(); |
||||
public override Task<string> GetLatestVersion() => Task.FromResult(string.Empty); |
||||
|
||||
public override Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) => Task.FromResult(Enumerable.Empty<PackageVersion>()); |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10) => Task.FromResult<IEnumerable<GitCommit>?>(null); |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task<IEnumerable<Branch>> GetAllBranches() => Task.FromResult(Enumerable.Empty<Branch>()); |
||||
|
||||
/// <inheritdoc /> |
||||
public override Task<IEnumerable<Release>> GetAllReleases() => Task.FromResult(Enumerable.Empty<Release>()); |
||||
} |
@ -0,0 +1,317 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Text.Json.Serialization; |
||||
using AsyncAwaitBestPractices; |
||||
using NLog; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Core.Models; |
||||
|
||||
[JsonSerializable(typeof(TrackedDownload))] |
||||
public class TrackedDownload |
||||
{ |
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||
|
||||
[JsonIgnore] |
||||
private IDownloadService? downloadService; |
||||
|
||||
[JsonIgnore] |
||||
private Task? downloadTask; |
||||
|
||||
[JsonIgnore] |
||||
private CancellationTokenSource? downloadCancellationTokenSource; |
||||
|
||||
[JsonIgnore] |
||||
private CancellationTokenSource? downloadPauseTokenSource; |
||||
|
||||
[JsonIgnore] |
||||
private CancellationTokenSource AggregateCancellationTokenSource => |
||||
CancellationTokenSource.CreateLinkedTokenSource( |
||||
downloadCancellationTokenSource?.Token ?? CancellationToken.None, |
||||
downloadPauseTokenSource?.Token ?? CancellationToken.None); |
||||
|
||||
public required Guid Id { get; init; } |
||||
|
||||
public required Uri SourceUrl { get; init; } |
||||
|
||||
public Uri? RedirectedUrl { get; init; } |
||||
|
||||
public required DirectoryPath DownloadDirectory { get; init; } |
||||
|
||||
public required string FileName { get; init; } |
||||
|
||||
public required string TempFileName { get; init; } |
||||
|
||||
public string? ExpectedHashSha256 { get; set; } |
||||
|
||||
[JsonIgnore] |
||||
[MemberNotNullWhen(true, nameof(ExpectedHashSha256))] |
||||
public bool ValidateHash => ExpectedHashSha256 is not null; |
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))] |
||||
public ProgressState ProgressState { get; set; } = ProgressState.Inactive; |
||||
|
||||
public List<string> ExtraCleanupFileNames { get; init; } = new(); |
||||
|
||||
// Used for restoring progress on load |
||||
public long DownloadedBytes { get; set; } |
||||
public long TotalBytes { get; set; } |
||||
|
||||
/// <summary> |
||||
/// Optional context action to be invoked on completion |
||||
/// </summary> |
||||
public IContextAction? ContextAction { get; set; } |
||||
|
||||
[JsonIgnore] |
||||
public Exception? Exception { get; private set; } |
||||
|
||||
#region Events |
||||
private WeakEventManager<ProgressReport>? progressUpdateEventManager; |
||||
|
||||
public event EventHandler<ProgressReport> ProgressUpdate |
||||
{ |
||||
add |
||||
{ |
||||
progressUpdateEventManager ??= new WeakEventManager<ProgressReport>(); |
||||
progressUpdateEventManager.AddEventHandler(value); |
||||
} |
||||
remove => progressUpdateEventManager?.RemoveEventHandler(value); |
||||
} |
||||
|
||||
protected void OnProgressUpdate(ProgressReport e) |
||||
{ |
||||
// Update downloaded and total bytes |
||||
DownloadedBytes = Convert.ToInt64(e.Current); |
||||
TotalBytes = Convert.ToInt64(e.Total); |
||||
|
||||
progressUpdateEventManager?.RaiseEvent(this, e, nameof(ProgressUpdate)); |
||||
} |
||||
|
||||
private WeakEventManager<ProgressState>? progressStateChangedEventManager; |
||||
|
||||
public event EventHandler<ProgressState> ProgressStateChanged |
||||
{ |
||||
add |
||||
{ |
||||
progressStateChangedEventManager ??= new WeakEventManager<ProgressState>(); |
||||
progressStateChangedEventManager.AddEventHandler(value); |
||||
} |
||||
remove => progressStateChangedEventManager?.RemoveEventHandler(value); |
||||
} |
||||
|
||||
protected void OnProgressStateChanged(ProgressState e) |
||||
{ |
||||
progressStateChangedEventManager?.RaiseEvent(this, e, nameof(ProgressStateChanged)); |
||||
} |
||||
#endregion |
||||
|
||||
[MemberNotNull(nameof(downloadService))] |
||||
private void EnsureDownloadService() |
||||
{ |
||||
if (downloadService == null) |
||||
{ |
||||
throw new InvalidOperationException("Download service is not set."); |
||||
} |
||||
} |
||||
|
||||
private async Task StartDownloadTask(long resumeFromByte, CancellationToken cancellationToken) |
||||
{ |
||||
var progress = new Progress<ProgressReport>(OnProgressUpdate); |
||||
|
||||
await downloadService!.ResumeDownloadToFileAsync( |
||||
SourceUrl.ToString(), |
||||
DownloadDirectory.JoinFile(TempFileName), |
||||
resumeFromByte, |
||||
progress, |
||||
cancellationToken: cancellationToken).ConfigureAwait(false); |
||||
|
||||
// If hash validation is enabled, validate the hash |
||||
if (ValidateHash) |
||||
{ |
||||
OnProgressUpdate(new ProgressReport(0, isIndeterminate: true, type: ProgressType.Hashing)); |
||||
var hash = await FileHash.GetSha256Async(DownloadDirectory.JoinFile(TempFileName), progress).ConfigureAwait(false); |
||||
if (hash != ExpectedHashSha256?.ToLowerInvariant()) |
||||
{ |
||||
throw new Exception($"Hash validation for {FileName} failed, expected {ExpectedHashSha256} but got {hash}"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void Start() |
||||
{ |
||||
if (ProgressState != ProgressState.Inactive) |
||||
{ |
||||
throw new InvalidOperationException($"Download state must be inactive to start, not {ProgressState}"); |
||||
} |
||||
Logger.Debug("Starting download {Download}", FileName); |
||||
|
||||
EnsureDownloadService(); |
||||
|
||||
downloadCancellationTokenSource = new CancellationTokenSource(); |
||||
downloadPauseTokenSource = new CancellationTokenSource(); |
||||
|
||||
downloadTask = StartDownloadTask(0, AggregateCancellationTokenSource.Token) |
||||
.ContinueWith(OnDownloadTaskCompleted); |
||||
|
||||
ProgressState = ProgressState.Working; |
||||
OnProgressStateChanged(ProgressState); |
||||
} |
||||
|
||||
public void Resume() |
||||
{ |
||||
if (ProgressState != ProgressState.Inactive) |
||||
{ |
||||
Logger.Warn("Attempted to resume download {Download} but it is not paused ({State})", FileName, ProgressState); |
||||
} |
||||
Logger.Debug("Resuming download {Download}", FileName); |
||||
|
||||
// Read the temp file to get the current size |
||||
var tempSize = 0L; |
||||
|
||||
var tempFile = DownloadDirectory.JoinFile(TempFileName); |
||||
if (tempFile.Exists) |
||||
{ |
||||
tempSize = tempFile.Info.Length; |
||||
} |
||||
|
||||
EnsureDownloadService(); |
||||
|
||||
downloadCancellationTokenSource = new CancellationTokenSource(); |
||||
downloadPauseTokenSource = new CancellationTokenSource(); |
||||
|
||||
downloadTask = StartDownloadTask(tempSize, AggregateCancellationTokenSource.Token) |
||||
.ContinueWith(OnDownloadTaskCompleted); |
||||
|
||||
ProgressState = ProgressState.Working; |
||||
OnProgressStateChanged(ProgressState); |
||||
} |
||||
|
||||
public void Pause() |
||||
{ |
||||
if (ProgressState != ProgressState.Working) |
||||
{ |
||||
Logger.Warn("Attempted to pause download {Download} but it is not in progress ({State})", FileName, ProgressState); |
||||
return; |
||||
} |
||||
|
||||
Logger.Debug("Pausing download {Download}", FileName); |
||||
downloadPauseTokenSource?.Cancel(); |
||||
} |
||||
|
||||
public void Cancel() |
||||
{ |
||||
if (ProgressState is not (ProgressState.Working or ProgressState.Inactive)) |
||||
{ |
||||
Logger.Warn("Attempted to cancel download {Download} but it is not in progress ({State})", FileName, ProgressState); |
||||
return; |
||||
} |
||||
|
||||
Logger.Debug("Cancelling download {Download}", FileName); |
||||
|
||||
// Cancel token if it exists |
||||
if (downloadCancellationTokenSource is { } token) |
||||
{ |
||||
token.Cancel(); |
||||
} |
||||
// Otherwise handle it manually |
||||
else |
||||
{ |
||||
DoCleanup(); |
||||
|
||||
ProgressState = ProgressState.Cancelled; |
||||
OnProgressStateChanged(ProgressState); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Deletes the temp file and any extra cleanup files |
||||
/// </summary> |
||||
private void DoCleanup() |
||||
{ |
||||
try |
||||
{ |
||||
DownloadDirectory.JoinFile(TempFileName).Delete(); |
||||
} |
||||
catch (IOException) |
||||
{ |
||||
Logger.Warn("Failed to delete temp file {TempFile}", TempFileName); |
||||
} |
||||
|
||||
foreach (var extraFile in ExtraCleanupFileNames) |
||||
{ |
||||
try |
||||
{ |
||||
DownloadDirectory.JoinFile(extraFile).Delete(); |
||||
} |
||||
catch (IOException) |
||||
{ |
||||
Logger.Warn("Failed to delete extra cleanup file {ExtraFile}", extraFile); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Invoked by the task's completion callback |
||||
/// </summary> |
||||
private void OnDownloadTaskCompleted(Task task) |
||||
{ |
||||
// For cancelled, check if it was actually cancelled or paused |
||||
if (task.IsCanceled) |
||||
{ |
||||
// If the task was cancelled, set the state to cancelled |
||||
if (downloadCancellationTokenSource?.IsCancellationRequested == true) |
||||
{ |
||||
ProgressState = ProgressState.Cancelled; |
||||
} |
||||
// If the task was not cancelled, set the state to paused |
||||
else if (downloadPauseTokenSource?.IsCancellationRequested == true) |
||||
{ |
||||
ProgressState = ProgressState.Inactive; |
||||
} |
||||
else |
||||
{ |
||||
throw new InvalidOperationException("Download task was cancelled but neither cancellation token was cancelled."); |
||||
} |
||||
} |
||||
// For faulted |
||||
else if (task.IsFaulted) |
||||
{ |
||||
// Set the exception |
||||
Exception = task.Exception; |
||||
|
||||
ProgressState = ProgressState.Failed; |
||||
} |
||||
// Otherwise success |
||||
else |
||||
{ |
||||
ProgressState = ProgressState.Success; |
||||
} |
||||
|
||||
// For failed or cancelled, delete the temp files |
||||
if (ProgressState is ProgressState.Failed or ProgressState.Cancelled) |
||||
{ |
||||
DoCleanup(); |
||||
} |
||||
else if (ProgressState == ProgressState.Success) |
||||
{ |
||||
// Move the temp file to the final file |
||||
DownloadDirectory.JoinFile(TempFileName).MoveTo(DownloadDirectory.JoinFile(FileName)); |
||||
} |
||||
|
||||
// For pause, just do nothing |
||||
|
||||
OnProgressStateChanged(ProgressState); |
||||
|
||||
// Dispose of the task and cancellation token |
||||
downloadTask = null; |
||||
downloadCancellationTokenSource = null; |
||||
downloadPauseTokenSource = null; |
||||
} |
||||
|
||||
public void SetDownloadService(IDownloadService service) |
||||
{ |
||||
downloadService = service; |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
using StabilityMatrix.Core.Models.Packages; |
||||
|
||||
namespace StabilityMatrix.Core.Models; |
||||
|
||||
public class UnknownInstalledPackage : InstalledPackage |
||||
{ |
||||
public static UnknownInstalledPackage FromDirectoryName(string name) |
||||
{ |
||||
return new UnknownInstalledPackage |
||||
{ |
||||
Id = Guid.NewGuid(), |
||||
PackageName = UnknownPackage.Key, |
||||
DisplayName = name, |
||||
LibraryPath = $"Packages{System.IO.Path.DirectorySeparatorChar}{name}", |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,15 @@
|
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
|
||||
namespace StabilityMatrix.Core.Services; |
||||
|
||||
public interface ITrackedDownloadService |
||||
{ |
||||
IEnumerable<TrackedDownload> Downloads { get; } |
||||
|
||||
event EventHandler<TrackedDownload>? DownloadAdded; |
||||
|
||||
TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath); |
||||
|
||||
TrackedDownload NewDownload(string downloadUrl, FilePath downloadPath) => NewDownload(new Uri(downloadUrl), downloadPath); |
||||
} |
@ -0,0 +1,247 @@
|
||||
using System.Collections.Concurrent; |
||||
using System.Text; |
||||
using System.Text.Json; |
||||
using Microsoft.Extensions.Logging; |
||||
using StabilityMatrix.Core.Database; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
|
||||
namespace StabilityMatrix.Core.Services; |
||||
|
||||
public class TrackedDownloadService : ITrackedDownloadService, IDisposable |
||||
{ |
||||
private readonly ILogger<TrackedDownloadService> logger; |
||||
private readonly IDownloadService downloadService; |
||||
private readonly ISettingsManager settingsManager; |
||||
|
||||
private readonly ConcurrentDictionary<Guid, (TrackedDownload, FileStream)> downloads = new(); |
||||
|
||||
public IEnumerable<TrackedDownload> Downloads => downloads.Values.Select(x => x.Item1); |
||||
|
||||
/// <inheritdoc /> |
||||
public event EventHandler<TrackedDownload>? DownloadAdded; |
||||
|
||||
public TrackedDownloadService( |
||||
ILogger<TrackedDownloadService> logger, |
||||
IDownloadService downloadService, |
||||
ISettingsManager settingsManager) |
||||
{ |
||||
this.logger = logger; |
||||
this.downloadService = downloadService; |
||||
this.settingsManager = settingsManager; |
||||
|
||||
// Index for in-progress downloads when library dir loaded |
||||
settingsManager.RegisterOnLibraryDirSet(path => |
||||
{ |
||||
var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); |
||||
// Ignore if not exist |
||||
if (!downloadsDir.Exists) return; |
||||
|
||||
LoadInProgressDownloads(downloadsDir); |
||||
}); |
||||
} |
||||
|
||||
private void OnDownloadAdded(TrackedDownload download) |
||||
{ |
||||
DownloadAdded?.Invoke(this, download); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Creates a new tracked download with backed json file and adds it to the dictionary. |
||||
/// </summary> |
||||
/// <param name="download"></param> |
||||
private void AddDownload(TrackedDownload download) |
||||
{ |
||||
// Set download service |
||||
download.SetDownloadService(downloadService); |
||||
|
||||
// Create json file |
||||
var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); |
||||
downloadsDir.Create(); |
||||
var jsonFile = downloadsDir.JoinFile($"{download.Id}.json"); |
||||
var jsonFileStream = jsonFile.Info.Open(FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read); |
||||
|
||||
// Serialize to json |
||||
var json = JsonSerializer.Serialize(download); |
||||
jsonFileStream.Write(Encoding.UTF8.GetBytes(json)); |
||||
jsonFileStream.Flush(); |
||||
|
||||
// Add to dictionary |
||||
downloads.TryAdd(download.Id, (download, jsonFileStream)); |
||||
|
||||
// Connect to state changed event to update json file |
||||
AttachHandlers(download); |
||||
|
||||
logger.LogDebug("Added download {Download}", download.FileName); |
||||
OnDownloadAdded(download); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Update the json file for the download. |
||||
/// </summary> |
||||
private void UpdateJsonForDownload(TrackedDownload download) |
||||
{ |
||||
// Serialize to json |
||||
var json = JsonSerializer.Serialize(download); |
||||
var jsonBytes = Encoding.UTF8.GetBytes(json); |
||||
|
||||
// Write to file |
||||
var (_, fs) = downloads[download.Id]; |
||||
fs.Seek(0, SeekOrigin.Begin); |
||||
fs.Write(jsonBytes); |
||||
fs.Flush(); |
||||
} |
||||
|
||||
private void AttachHandlers(TrackedDownload download) |
||||
{ |
||||
download.ProgressStateChanged += TrackedDownload_OnProgressStateChanged; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Handler when the download's state changes |
||||
/// </summary> |
||||
private void TrackedDownload_OnProgressStateChanged(object? sender, ProgressState e) |
||||
{ |
||||
if (sender is not TrackedDownload download) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
// Update json file |
||||
UpdateJsonForDownload(download); |
||||
|
||||
// If the download is completed, remove it from the dictionary and delete the json file |
||||
if (e is ProgressState.Success or ProgressState.Failed or ProgressState.Cancelled) |
||||
{ |
||||
if (downloads.TryRemove(download.Id, out var downloadInfo)) |
||||
{ |
||||
downloadInfo.Item2.Dispose(); |
||||
// Delete json file |
||||
new DirectoryPath(settingsManager.DownloadsDirectory).JoinFile($"{download.Id}.json").Delete(); |
||||
logger.LogDebug("Removed download {Download}", download.FileName); |
||||
} |
||||
} |
||||
|
||||
// On successes, run the continuation action |
||||
if (e == ProgressState.Success) |
||||
{ |
||||
if (download.ContextAction is CivitPostDownloadContextAction action) |
||||
{ |
||||
logger.LogDebug("Running context action for {Download}", download.FileName); |
||||
action.Invoke(settingsManager); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void LoadInProgressDownloads(DirectoryPath downloadsDir) |
||||
{ |
||||
logger.LogDebug("Indexing in-progress downloads at {DownloadsDir}...", downloadsDir); |
||||
|
||||
var jsonFiles = downloadsDir.Info.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly); |
||||
|
||||
// Add to dictionary, the file name is the guid |
||||
foreach (var file in jsonFiles) |
||||
{ |
||||
// Try to get a shared write handle |
||||
try |
||||
{ |
||||
var fileStream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.Read); |
||||
|
||||
// Deserialize json and add to dictionary |
||||
var download = JsonSerializer.Deserialize<TrackedDownload>(fileStream)!; |
||||
|
||||
// If the download is marked as working, pause it |
||||
if (download.ProgressState == ProgressState.Working) |
||||
{ |
||||
download.ProgressState = ProgressState.Inactive; |
||||
} |
||||
else if (download.ProgressState != ProgressState.Inactive) |
||||
{ |
||||
// If the download is not inactive, skip it |
||||
logger.LogWarning("Skipping download {Download} with state {State}", download.FileName, download.ProgressState); |
||||
fileStream.Dispose(); |
||||
|
||||
// Delete json file |
||||
logger.LogDebug("Deleting json file for {Download} with unsupported state", download.FileName); |
||||
file.Delete(); |
||||
continue; |
||||
} |
||||
|
||||
download.SetDownloadService(downloadService); |
||||
|
||||
downloads.TryAdd(download.Id, (download, fileStream)); |
||||
|
||||
AttachHandlers(download); |
||||
|
||||
OnDownloadAdded(download); |
||||
|
||||
logger.LogDebug("Loaded in-progress download {Download}", download.FileName); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
logger.LogInformation(e, "Could not open file {File} for reading", file.Name); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath) |
||||
{ |
||||
var download = new TrackedDownload |
||||
{ |
||||
Id = Guid.NewGuid(), |
||||
SourceUrl = downloadUrl, |
||||
DownloadDirectory = downloadPath.Directory!, |
||||
FileName = downloadPath.Name, |
||||
TempFileName = NewTempFileName(downloadPath.Directory!), |
||||
}; |
||||
|
||||
AddDownload(download); |
||||
|
||||
return download; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Generate a new temp file name that is unique in the given directory. |
||||
/// In format of "Unconfirmed {id}.smdownload" |
||||
/// </summary> |
||||
/// <param name="parentDir"></param> |
||||
/// <returns></returns> |
||||
private static string NewTempFileName(DirectoryPath parentDir) |
||||
{ |
||||
FilePath? tempFile = null; |
||||
|
||||
for (var i = 0; i < 10; i++) |
||||
{ |
||||
if (tempFile is {Exists: false}) |
||||
{ |
||||
return tempFile.Name; |
||||
} |
||||
var id = Random.Shared.Next(1000000, 9999999); |
||||
tempFile = parentDir.JoinFile($"Unconfirmed {id}.smdownload"); |
||||
} |
||||
|
||||
throw new Exception("Failed to generate a unique temp file name."); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void Dispose() |
||||
{ |
||||
foreach (var (download, fs) in downloads.Values) |
||||
{ |
||||
if (download.ProgressState == ProgressState.Working) |
||||
{ |
||||
try |
||||
{ |
||||
download.Pause(); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
logger.LogWarning(e, "Failed to pause download {Download}", download.FileName); |
||||
} |
||||
} |
||||
} |
||||
|
||||
GC.SuppressFinalize(this); |
||||
} |
||||
} |
Loading…
Reference in new issue