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. 45
      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. 19
      StabilityMatrix.Avalonia/Services/NotificationService.cs
  11. 12
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  12. 41
      StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs
  13. 5
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  14. 7
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  15. 5
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  16. 36
      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:
environment: production
ignore_missing: true
version: StabilityMatrix@${{ env.GIT_TAG_NAME }}
version: StabilityMatrix.Avalonia@${{ env.GIT_TAG_NAME }}
- name: Create Sentry release
if: ${{ github.event_name == 'workflow_dispatch' }}
@ -97,7 +97,7 @@ jobs:
with:
environment: production
ignore_missing: true
version: StabilityMatrix@${{ github.event.inputs.version }}
version: StabilityMatrix.Avalonia@${{ github.event.inputs.version }}
release-windows:
@ -143,7 +143,6 @@ jobs:
-o out -c Release -r ${{ env.platform-id }}
-p:Version=$env:RELEASE_VERSION
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
-p:PublishTrimmed=true
-p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }}
-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: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
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
# publish, but before the final output is built. This could, for example, be a script which copies

45
StabilityMatrix.Avalonia/App.axaml.cs

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

9
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -151,6 +151,8 @@ public static class DesignData
{
FilePath = "~/Models/Lora/electricity-light.safetensors",
Title = "Auroral Background",
PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" +
"78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo
{
VersionName = "Lightning Auroral",
@ -170,7 +172,7 @@ public static class DesignData
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
},
}
},
},
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
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.Controls.Notifications;
using StabilityMatrix.Avalonia.Services;
@ -29,7 +30,18 @@ public class MockNotificationService : INotificationService
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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@ -16,6 +18,7 @@ using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using Semver;
using Sentry;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper;
@ -27,7 +30,7 @@ namespace StabilityMatrix.Avalonia;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
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
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
@ -35,6 +38,11 @@ public class Program
[STAThread]
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();
var infoVersion = Assembly.GetExecutingAssembly()
@ -42,14 +50,13 @@ public class Program
Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict);
// 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;
}
// Configure Sentry
if ((Debugger.IsAttached && args.Contains("--debug-sentry")) || !args.Contains("--no-sentry"))
if (!Args.NoSentry && (!Debugger.IsAttached || Args.DebugSentry))
{
ConfigureSentry();
}
@ -61,6 +68,9 @@ public class Program
public static AppBuilder BuildAvaloniaApp()
{
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>()
.UsePlatformDetect()
@ -89,6 +99,12 @@ public class Program
{
currentExe.CopyTo(targetExe, true);
// Ensure permissions are set for unix
if (Compat.IsUnix)
{
File.SetUnixFileMode(targetExe, (UnixFileMode) 0x755);
}
// Start the new app
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.Controls.Notifications;
using StabilityMatrix.Core.Models;
@ -41,6 +42,23 @@ public interface INotificationService
string? message = null,
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);
}

19
StabilityMatrix.Avalonia/Services/NotificationService.cs

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

12
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

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

41
StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs

@ -39,10 +39,12 @@ public partial class CheckpointFile : ViewModelBase
/// </summary>
[ObservableProperty]
private string title = string.Empty;
public string? PreviewImagePath { get; set; }
public Bitmap? PreviewImage { get; set; }
public bool IsPreviewImageLoaded => PreviewImage != null;
/// <summary>
/// Path to preview image. Can be local or remote URL.
/// </summary>
[ObservableProperty]
private string? previewImagePath;
[ObservableProperty]
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]
private async Task DeleteAsync()
{
@ -89,6 +103,14 @@ public partial class CheckpointFile : ViewModelBase
{
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)
{
@ -169,16 +191,6 @@ public partial class CheckpointFile : ViewModelBase
if (ConnectedModel?.ModelId == null) return;
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>
/// Indexes directory and yields all checkpoint files.
@ -226,7 +238,6 @@ public partial class CheckpointFile : ViewModelBase
if (previewImage != null)
{
checkpointFile.PreviewImagePath = files[previewImage];
checkpointFile.LoadPreviewImage().SafeFireAndForget();
}
yield return checkpointFile;

5
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

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

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

@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
@ -78,6 +79,12 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
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...";
await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds...";

5
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

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

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

@ -1,8 +1,11 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
@ -54,9 +57,27 @@ public partial class MainWindow : AppWindowBase
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
// Initialize notification service using this window as the visual root
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)
{
if (IsWindows11)
@ -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()
{
TransparencyBackgroundFallback = Brushes.Transparent;

1
StabilityMatrix.Core/StabilityMatrix.Core.csproj

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

13
StabilityMatrix.Core/Updater/UpdateHelper.cs

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

Loading…
Cancel
Save