Ionite 1 year ago
parent
commit
538e5bf673
No known key found for this signature in database
  1. 5
      .github/workflows/release.yml
  2. 2
      StabilityMatrix.Avalonia.pupnet.conf
  3. 41
      StabilityMatrix.Avalonia/App.axaml.cs
  4. 9
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  5. 16
      StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs
  6. 70
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  7. 28
      StabilityMatrix.Avalonia/Models/AppArgs.cs
  8. 24
      StabilityMatrix.Avalonia/Program.cs
  9. 22
      StabilityMatrix.Avalonia/Services/INotificationService.cs
  10. 17
      StabilityMatrix.Avalonia/Services/NotificationService.cs
  11. 12
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  12. 39
      StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs
  13. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  14. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  15. 5
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  16. 34
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  17. 1
      StabilityMatrix.Core/StabilityMatrix.Core.csproj
  18. 13
      StabilityMatrix.Core/Updater/UpdateHelper.cs

5
.github/workflows/release.yml

@ -84,7 +84,7 @@ jobs:
with: with:
environment: production environment: production
ignore_missing: true ignore_missing: true
version: StabilityMatrix@${{ env.GIT_TAG_NAME }} version: StabilityMatrix.Avalonia@${{ env.GIT_TAG_NAME }}
- name: Create Sentry release - name: Create Sentry release
if: ${{ github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'workflow_dispatch' }}
@ -97,7 +97,7 @@ jobs:
with: with:
environment: production environment: production
ignore_missing: true ignore_missing: true
version: StabilityMatrix@${{ github.event.inputs.version }} version: StabilityMatrix.Avalonia@${{ github.event.inputs.version }}
release-windows: release-windows:
@ -143,7 +143,6 @@ jobs:
-o out -c Release -r ${{ env.platform-id }} -o out -c Release -r ${{ env.platform-id }}
-p:Version=$env:RELEASE_VERSION -p:Version=$env:RELEASE_VERSION
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
-p:PublishTrimmed=true
-p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }} -p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }}
-p:SentryUploadSymbols=true -p:SentryUploadSources=true -p:SentryUploadSymbols=true -p:SentryUploadSources=true

2
StabilityMatrix.Avalonia.pupnet.conf

@ -138,7 +138,7 @@ DotnetProjectPath = StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
# '-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishReadyToRun=true # '-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishReadyToRun=true
# -p:PublishTrimmed=true -p:TrimMode=link'. Note. This value may use macro variables. Use 'pupnet --help macro' # -p:PublishTrimmed=true -p:TrimMode=link'. Note. This value may use macro variables. Use 'pupnet --help macro'
# for reference. See: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish # for reference. See: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish
DotnetPublishArgs = -p:Version=${APP_VERSION} --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -p:PublishTrimmed=true DotnetPublishArgs = -p:Version=${APP_VERSION} -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false --self-contained
# Post-publish (or standalone build) command on Linux (ignored on Windows). It is called after dotnet # Post-publish (or standalone build) command on Linux (ignored on Windows). It is called after dotnet
# publish, but before the final output is built. This could, for example, be a script which copies # publish, but before the final output is built. This could, for example, be a script which copies

41
StabilityMatrix.Avalonia/App.axaml.cs

@ -13,6 +13,7 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Styling; using Avalonia.Styling;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -62,6 +63,7 @@ public sealed class App : Application
[NotNull] public static IServiceProvider? Services { get; private set; } [NotNull] public static IServiceProvider? Services { get; private set; }
[NotNull] public static Visual? VisualRoot { get; private set; } [NotNull] public static Visual? VisualRoot { get; private set; }
[NotNull] public static IStorageProvider? StorageProvider { get; private set; } [NotNull] public static IStorageProvider? StorageProvider { get; private set; }
// ReSharper disable once MemberCanBePrivate.Global
[NotNull] public static IConfiguration? Config { get; private set; } [NotNull] public static IConfiguration? Config { get; private set; }
// ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global
@ -108,6 +110,9 @@ public sealed class App : Application
setupWindow.ShowActivated = true; setupWindow.ShowActivated = true;
setupWindow.ShowAsyncCts = new CancellationTokenSource(); setupWindow.ShowAsyncCts = new CancellationTokenSource();
setupWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ?
ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome;
DesktopLifetime.MainWindow = setupWindow; DesktopLifetime.MainWindow = setupWindow;
setupWindow.ShowAsyncCts.Token.Register(() => setupWindow.ShowAsyncCts.Token.Register(() =>
@ -142,6 +147,9 @@ public sealed class App : Application
mainWindow.DataContext = mainViewModel; mainWindow.DataContext = mainViewModel;
mainWindow.NotificationService = notificationService; mainWindow.NotificationService = notificationService;
mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ?
ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome;
var settingsManager = Services.GetRequiredService<ISettingsManager>(); var settingsManager = Services.GetRequiredService<ISettingsManager>();
var windowSettings = settingsManager.Settings.WindowSettings; var windowSettings = settingsManager.Settings.WindowSettings;
if (windowSettings != null) if (windowSettings != null)
@ -475,11 +483,13 @@ public sealed class App : Application
private static LoggingConfiguration ConfigureLogging() private static LoggingConfiguration ConfigureLogging()
{ {
var logConfig = new LoggingConfiguration(); LogManager.Setup().LoadConfiguration(builder => {
var debugTarget = builder.ForTarget("console").WriteTo(new DebuggerTarget
{
Layout = "${message}"
}).WithAsync();
// File target var fileTarget = builder.ForTarget("logfile").WriteTo(new FileTarget
logConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal,
new FileTarget("logfile")
{ {
Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}", Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
ArchiveOldFileOnStartup = true, ArchiveOldFileOnStartup = true,
@ -487,19 +497,21 @@ public sealed class App : Application
ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveNumbering = ArchiveNumberingMode.Rolling, ArchiveNumbering = ArchiveNumberingMode.Rolling,
MaxArchiveFiles = 2 MaxArchiveFiles = 2
}); }).WithAsync();
// Debugger Target // Filter some sources to be warn levels or above only
logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, builder.ForLogger("System.*").WriteToNil(NLog.LogLevel.Warn);
new DebuggerTarget("debugger") builder.ForLogger("Microsoft.*").WriteToNil(NLog.LogLevel.Warn);
{ builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
Layout = "${message}"
}); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget);
});
// Sentry // Sentry
if (SentrySdk.IsEnabled) if (SentrySdk.IsEnabled)
{ {
logConfig.AddSentry(o => LogManager.Configuration.AddSentry(o =>
{ {
o.InitializeSdk = false; o.InitializeSdk = false;
o.Layout = "${message}"; o.Layout = "${message}";
@ -513,9 +525,6 @@ public sealed class App : Application
}); });
} }
LogManager.Configuration = logConfig; return LogManager.Configuration;
return logConfig;
} }
} }

9
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -151,6 +151,8 @@ public static class DesignData
{ {
FilePath = "~/Models/Lora/electricity-light.safetensors", FilePath = "~/Models/Lora/electricity-light.safetensors",
Title = "Auroral Background", Title = "Auroral Background",
PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" +
"78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo ConnectedModel = new ConnectedModelInfo
{ {
VersionName = "Lightning Auroral", VersionName = "Lightning Auroral",
@ -170,7 +172,7 @@ public static class DesignData
FilePath = "~/Models/Lora/model.safetensors", FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model" Title = "Some model"
}, },
} },
}, },
new(settingsManager, downloadService, modelFinder) new(settingsManager, downloadService, modelFinder)
{ {
@ -187,6 +189,11 @@ public static class DesignData
} }
}; };
foreach (var folder in CheckpointsPageViewModel.CheckpointFolders)
{
folder.DisplayedCheckpointFiles = folder.CheckpointFiles;
}
CheckpointBrowserViewModel.ModelCards = new CheckpointBrowserViewModel.ModelCards = new
ObservableCollection<CheckpointBrowserCardViewModel> ObservableCollection<CheckpointBrowserCardViewModel>
{ {

16
StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
@ -29,7 +30,18 @@ public class MockNotificationService : INotificationService
return Task.FromResult(new TaskResult<bool>(true)); return Task.FromResult(new TaskResult<bool>(true));
} }
public void Show(string title, string message, NotificationType appearance = NotificationType.Information) public void Show(
string title,
string message,
NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null)
{
}
public void ShowPersistent(
string title,
string message,
NotificationType appearance = NotificationType.Information)
{ {
} }
} }

70
StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs

@ -0,0 +1,70 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader.Loaders;
using Avalonia.Media.Imaging;
namespace StabilityMatrix.Avalonia;
public readonly record struct ImageLoadFailedEventArgs(string Url, Exception Exception);
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
{
private readonly WeakEventManager<ImageLoadFailedEventArgs> loadFailedEventManager = new();
public event EventHandler<ImageLoadFailedEventArgs> LoadFailed
{
add => loadFailedEventManager.AddEventHandler(value);
remove => loadFailedEventManager.RemoveEventHandler(value);
}
protected void OnLoadFailed(string url, Exception exception) => loadFailedEventManager.RaiseEvent(
this, new ImageLoadFailedEventArgs(url, exception), nameof(LoadFailed));
/// <summary>
/// Attempts to load bitmap
/// </summary>
/// <param name="url">Target url</param>
/// <returns>Bitmap</returns>
protected override async Task<Bitmap?> LoadAsync(string url)
{
// Try to load from local file first
if (File.Exists(url))
{
try
{
return new Bitmap(url);
}
catch (Exception e)
{
OnLoadFailed(url, e);
return null;
}
}
var internalOrCachedBitmap =
await LoadFromInternalAsync(url).ConfigureAwait(false)
?? await LoadFromGlobalCache(url).ConfigureAwait(false);
if (internalOrCachedBitmap != null) return internalOrCachedBitmap;
try
{
var externalBytes = await LoadDataFromExternalAsync(url).ConfigureAwait(false);
if (externalBytes == null) return null;
using var memoryStream = new MemoryStream(externalBytes);
var bitmap = new Bitmap(memoryStream);
await SaveToGlobalCache(url, externalBytes).ConfigureAwait(false);
return bitmap;
}
catch (Exception)
{
return null;
}
}
}

28
StabilityMatrix.Avalonia/Models/AppArgs.cs

@ -0,0 +1,28 @@
namespace StabilityMatrix.Avalonia.Models;
/// <summary>
/// Command line arguments passed to the application.
/// </summary>
public class AppArgs
{
/// <summary>
/// Whether to use the exception dialog while debugger is attached.
/// When no debugger is attached, the exception dialog is always used.
/// </summary>
public bool DebugExceptionDialog { get; set; }
/// <summary>
/// Whether to use Sentry when a debugger is attached.
/// </summary>
public bool DebugSentry { get; set; }
/// <summary>
/// Whether to disable Sentry.
/// </summary>
public bool NoSentry { get; set; }
/// <summary>
/// Whether to disable window chrome effects
/// </summary>
public bool NoWindowChromeEffects { get; set; }
}

24
StabilityMatrix.Avalonia/Program.cs

@ -1,11 +1,13 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncImageLoader;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
@ -16,6 +18,7 @@ using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.FontAwesome;
using Semver; using Semver;
using Sentry; using Sentry;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -27,7 +30,7 @@ namespace StabilityMatrix.Avalonia;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class Program public class Program
{ {
private static bool isExceptionDialogEnabled; public static AppArgs Args { get; } = new();
// Initialization code. Don't use any Avalonia, third-party APIs or any // Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
@ -35,6 +38,11 @@ public class Program
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{ {
Args.DebugExceptionDialog = args.Contains("--debug-exception-dialog");
Args.DebugSentry = args.Contains("--debug-sentry");
Args.NoSentry = args.Contains("--no-sentry");
Args.NoWindowChromeEffects = args.Contains("--no-window-chrome-effects");
HandleUpdateReplacement(); HandleUpdateReplacement();
var infoVersion = Assembly.GetExecutingAssembly() var infoVersion = Assembly.GetExecutingAssembly()
@ -42,14 +50,13 @@ public class Program
Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict); Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict);
// Configure exception dialog for unhandled exceptions // Configure exception dialog for unhandled exceptions
if (!Debugger.IsAttached || args.Contains("--debug-exception-dialog")) if (!Debugger.IsAttached || Args.DebugExceptionDialog)
{ {
isExceptionDialogEnabled = true;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
} }
// Configure Sentry // Configure Sentry
if ((Debugger.IsAttached && args.Contains("--debug-sentry")) || !args.Contains("--no-sentry")) if (!Args.NoSentry && (!Debugger.IsAttached || Args.DebugSentry))
{ {
ConfigureSentry(); ConfigureSentry();
} }
@ -61,6 +68,9 @@ public class Program
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
{ {
IconProvider.Current.Register<FontAwesomeIconProvider>(); IconProvider.Current.Register<FontAwesomeIconProvider>();
// Use our custom image loader for custom local load error handling
ImageLoader.AsyncImageLoader.Dispose();
ImageLoader.AsyncImageLoader = new FallbackRamCachedWebImageLoader();
return AppBuilder.Configure<App>() return AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
@ -89,6 +99,12 @@ public class Program
{ {
currentExe.CopyTo(targetExe, true); currentExe.CopyTo(targetExe, true);
// Ensure permissions are set for unix
if (Compat.IsUnix)
{
File.SetUnixFileMode(targetExe, (UnixFileMode) 0x755);
}
// Start the new app // Start the new app
Process.Start(targetExe); Process.Start(targetExe);

22
StabilityMatrix.Avalonia/Services/INotificationService.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -41,6 +42,23 @@ public interface INotificationService
string? message = null, string? message = null,
NotificationType appearance = NotificationType.Error); NotificationType appearance = NotificationType.Error);
void Show(string title, string message, /// <summary>
/// Show a notification with the given parameters.
/// </summary>
void Show(
string title,
string message,
NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null);
/// <summary>
/// Show a notification that will not auto-dismiss.
/// </summary>
/// <param name="title"></param>
/// <param name="message"></param>
/// <param name="appearance"></param>
void ShowPersistent(
string title,
string message,
NotificationType appearance = NotificationType.Information); NotificationType appearance = NotificationType.Information);
} }

17
StabilityMatrix.Avalonia/Services/NotificationService.cs

@ -14,7 +14,7 @@ public class NotificationService : INotificationService
public void Initialize( public void Initialize(
Visual? visual, Visual? visual,
NotificationPosition position = NotificationPosition.BottomRight, NotificationPosition position = NotificationPosition.BottomRight,
int maxItems = 3) int maxItems = 4)
{ {
if (notificationManager is not null) return; if (notificationManager is not null) return;
notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(visual)) notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(visual))
@ -29,10 +29,21 @@ public class NotificationService : INotificationService
notificationManager?.Show(notification); notificationManager?.Show(notification);
} }
public void Show(string title, string message, public void Show(
string title,
string message,
NotificationType appearance = NotificationType.Information,
TimeSpan? expiration = null)
{
Show(new Notification(title, message, appearance, expiration));
}
public void ShowPersistent(
string title,
string message,
NotificationType appearance = NotificationType.Information) NotificationType appearance = NotificationType.Information)
{ {
Show(new Notification(title, message, appearance)); Show(new Notification(title, message, appearance, TimeSpan.Zero));
} }
/// <inheritdoc /> /// <inheritdoc />

12
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -7,15 +7,13 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<TrimMode>partial</TrimMode>
<PublishTrimmed>true</PublishTrimmed>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon> <ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.0.0-dev.1</Version> <Version>2.0.0-dev.1</Version>
<InformationalVersion>$(Version)</InformationalVersion> <InformationalVersion>$(Version)</InformationalVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.0.0-avalonia11-preview6" /> <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.0.0" />
<PackageReference Include="Avalonia" Version="11.0.0" /> <PackageReference Include="Avalonia" Version="11.0.0" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.0" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
@ -50,16 +48,16 @@
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\Icon.ico" /> <AvaloniaResource Include="Assets\Icon.ico" />
<AvaloniaResource Include="Assets\Icon.png" /> <AvaloniaResource Include="Assets\Icon.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\StabilityMatrix.Core\StabilityMatrix.Core.csproj" /> <ProjectReference Include="..\StabilityMatrix.Core\StabilityMatrix.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\noimage.png" /> <AvaloniaResource Include="Assets\noimage.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

39
StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs

@ -40,9 +40,11 @@ public partial class CheckpointFile : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string title = string.Empty; private string title = string.Empty;
public string? PreviewImagePath { get; set; } /// <summary>
public Bitmap? PreviewImage { get; set; } /// Path to preview image. Can be local or remote URL.
public bool IsPreviewImageLoaded => PreviewImage != null; /// </summary>
[ObservableProperty]
private string? previewImagePath;
[ObservableProperty] [ObservableProperty]
private ConnectedModelInfo? connectedModel; private ConnectedModelInfo? connectedModel;
@ -75,6 +77,18 @@ public partial class CheckpointFile : ViewModelBase
} }
} }
private string GetConnectedModelInfoFilePath()
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException(
"Cannot get connected model info file path when FilePath is empty");
}
var modelNameNoExt = Path.GetFileNameWithoutExtension(FilePath);
var modelDir = Path.GetDirectoryName(FilePath) ?? "";
return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json");
}
[RelayCommand] [RelayCommand]
private async Task DeleteAsync() private async Task DeleteAsync()
{ {
@ -89,6 +103,14 @@ public partial class CheckpointFile : ViewModelBase
{ {
await Task.Run(() => File.Delete(PreviewImagePath)); await Task.Run(() => File.Delete(PreviewImagePath));
} }
if (ConnectedModel != null)
{
var cmInfoPath = GetConnectedModelInfoFilePath();
if (File.Exists(cmInfoPath))
{
await Task.Run(() => File.Delete(cmInfoPath));
}
}
} }
catch (IOException ex) catch (IOException ex)
{ {
@ -170,16 +192,6 @@ public partial class CheckpointFile : ViewModelBase
ProcessRunner.OpenUrl($"https://civitai.com/models/{ConnectedModel.ModelId}"); ProcessRunner.OpenUrl($"https://civitai.com/models/{ConnectedModel.ModelId}");
} }
// Loads image from path
private async Task LoadPreviewImage()
{
if (PreviewImagePath == null) return;
await Dispatcher.UIThread.InvokeAsync(() =>
{
PreviewImage = new Bitmap(File.OpenRead(PreviewImagePath));
});
}
/// <summary> /// <summary>
/// Indexes directory and yields all checkpoint files. /// Indexes directory and yields all checkpoint files.
/// First we match all files with supported extensions. /// First we match all files with supported extensions.
@ -226,7 +238,6 @@ public partial class CheckpointFile : ViewModelBase
if (previewImage != null) if (previewImage != null)
{ {
checkpointFile.PreviewImagePath = files[previewImage]; checkpointFile.PreviewImagePath = files[previewImage];
checkpointFile.LoadPreviewImage().SafeFireAndForget();
} }
yield return checkpointFile; yield return checkpointFile;

3
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -67,12 +67,13 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
DisplayedCheckpointFolders = CheckpointFolders; DisplayedCheckpointFolders = CheckpointFolders;
if (Design.IsDesignMode) return;
// Set UI states // Set UI states
IsImportAsConnected = settingsManager.Settings.IsImportAsConnected; IsImportAsConnected = settingsManager.Settings.IsImportAsConnected;
SearchFilter = string.Empty; SearchFilter = string.Empty;
if (Design.IsDesignMode) return;
IsLoading = CheckpointFolders.Count == 0; IsLoading = CheckpointFolders.Count == 0;
IsIndexing = CheckpointFolders.Count > 0; IsIndexing = CheckpointFolders.Count > 0;
await IndexFolders(); await IndexFolders();

7
StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
@ -78,6 +79,12 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
ProgressValue = Convert.ToInt32(report.Percentage); ProgressValue = Convert.ToInt32(report.Percentage);
})); }));
// On unix, we need to set the executable bit
if (Compat.IsUnix)
{
File.SetUnixFileMode(UpdateHelper.ExecutablePath, (UnixFileMode) 0x755);
}
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds..."; UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";
await Task.Delay(1000); await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds..."; UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds...";

5
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -101,9 +101,10 @@
TextWrapping="WrapWithOverflow" TextWrapping="WrapWithOverflow"
IsVisible="{Binding IsConnectedModel}" /> IsVisible="{Binding IsConnectedModel}" />
<!-- Image --> <!-- Image -->
<Image <controls:BetterAdvancedImage
Margin="0,0,0,4" Margin="0,0,0,4"
Source="{Binding PreviewImage}" CornerRadius="4"
Source="{Binding PreviewImagePath}"
Stretch="Uniform" Stretch="Uniform"
IsVisible="{Binding IsConnectedModel}" /> IsVisible="{Binding IsConnectedModel}" />
</StackPanel> </StackPanel>

34
StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

@ -1,8 +1,11 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
@ -54,7 +57,25 @@ public partial class MainWindow : AppWindowBase
protected override void OnLoaded(RoutedEventArgs e) protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(e); base.OnLoaded(e);
// Initialize notification service using this window as the visual root
NotificationService?.Initialize(this); NotificationService?.Initialize(this);
// Attach error notification handler for image loader
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.LoadFailed += OnImageLoadFailed;
}
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
// Detach error notification handler for image loader
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.LoadFailed -= OnImageLoadFailed;
}
} }
private void OnActualThemeVariantChanged(object? sender, EventArgs e) private void OnActualThemeVariantChanged(object? sender, EventArgs e)
@ -73,6 +94,19 @@ public partial class MainWindow : AppWindowBase
} }
} }
private void OnImageLoadFailed(object? sender, ImageLoadFailedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
var fileName = Path.GetFileName(e.Url);
var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName;
NotificationService?.ShowPersistent(
"Failed to load image",
$"Could not load '{displayName}'\n({e.Exception.Message})",
NotificationType.Warning);
});
}
private void TryEnableMicaEffect() private void TryEnableMicaEffect()
{ {
TransparencyBackgroundFallback = Brushes.Transparent; TransparencyBackgroundFallback = Brushes.Transparent;

1
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -5,6 +5,7 @@
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

13
StabilityMatrix.Core/Updater/UpdateHelper.cs

@ -35,14 +35,14 @@ public class UpdateHelper : IUpdateHelper
this.downloadService = downloadService; this.downloadService = downloadService;
this.debugOptions = debugOptions.Value; this.debugOptions = debugOptions.Value;
timer.Elapsed += async (_, _) => { await CheckForUpdate(); }; timer.Elapsed += async (_, _) => { await CheckForUpdate().ConfigureAwait(false); };
} }
public async Task StartCheckingForUpdates() public async Task StartCheckingForUpdates()
{ {
timer.Enabled = true; timer.Enabled = true;
timer.Start(); timer.Start();
await CheckForUpdate(); await CheckForUpdate().ConfigureAwait(false);
} }
public async Task DownloadUpdate(UpdateInfo updateInfo, public async Task DownloadUpdate(UpdateInfo updateInfo,
@ -54,7 +54,7 @@ public class UpdateHelper : IUpdateHelper
// download the file from URL // download the file from URL
await downloadService.DownloadToFileAsync(downloadUrl, ExecutablePath, progress: progress, await downloadService.DownloadToFileAsync(downloadUrl, ExecutablePath, progress: progress,
httpClientName: "UpdateClient"); httpClientName: "UpdateClient").ConfigureAwait(false);
} }
@ -77,17 +77,18 @@ public class UpdateHelper : IUpdateHelper
try try
{ {
var httpClient = httpClientFactory.CreateClient("UpdateClient"); var httpClient = httpClientFactory.CreateClient("UpdateClient");
var response = await httpClient.GetAsync(UpdateManifestUrl); var response = await httpClient.GetAsync(UpdateManifestUrl).ConfigureAwait(false);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
logger.LogWarning("Error while checking for update {StatusCode} - {Content}", logger.LogWarning("Error while checking for update {StatusCode} - {Content}",
response.StatusCode, await response.Content.ReadAsStringAsync()); response.StatusCode, await response.Content.ReadAsStringAsync().ConfigureAwait(false));
return; return;
} }
var updateCollection = var updateCollection =
await JsonSerializer.DeserializeAsync<UpdateCollection>( await JsonSerializer.DeserializeAsync<UpdateCollection>(
await response.Content.ReadAsStreamAsync()); await response.Content.ReadAsStreamAsync()
.ConfigureAwait(false)).ConfigureAwait(false);
if (updateCollection is null) if (updateCollection is null)
{ {

Loading…
Cancel
Save