Browse Source
# Conflicts: # StabilityMatrix.Avalonia/App.axaml # StabilityMatrix.Avalonia/DesignData/DesignData.cs # StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs # StabilityMatrix.Avalonia/Views/SettingsPage.axaml # StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs # StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs # StabilityMatrix.Core/Services/ISettingsManager.cs # StabilityMatrix.Core/Services/SettingsManager.cspull/165/head
Ionite
1 year ago
41 changed files with 2181 additions and 321 deletions
@ -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,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,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,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("To see the GUI go to", 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,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,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