JT
1 year ago
committed by
GitHub
34 changed files with 1961 additions and 258 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,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,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,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