Multi-Platform Package Manager for Stable Diffusion
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

455 lines
15 KiB

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Interop;
using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Windowing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
1 year ago
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Core.Attributes;
12 months ago
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using ILogger = NLog.ILogger;
using TeachingTip = FluentAvalonia.UI.Controls.TeachingTip;
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.Views;
#endif
namespace StabilityMatrix.Avalonia.Views;
[SuppressMessage("ReSharper", "UnusedParameter.Local")]
[Singleton]
public partial class MainWindow : AppWindowBase
{
private readonly INotificationService notificationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ISettingsManager settingsManager;
private readonly ILogger<MainWindow> logger;
private FlyoutBase? progressFlyout;
[DesignOnly(true)]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public MainWindow()
{
notificationService = null!;
navigationService = null!;
}
public MainWindow(
INotificationService notificationService,
INavigationService<MainWindowViewModel> navigationService,
ISettingsManager settingsManager,
ILogger<MainWindow> logger
)
{
this.notificationService = notificationService;
this.navigationService = navigationService;
this.settingsManager = settingsManager;
this.logger = logger;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
this.AttachDebugSaveScreenshot();
1 year ago
LogWindow.Attach(this, App.Services);
#endif
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
navigationService.TypedNavigation += NavigationService_OnTypedNavigation;
EventManager.Instance.ToggleProgressFlyout += (_, _) => progressFlyout?.Hide();
EventManager.Instance.CultureChanged += (_, _) => SetDefaultFonts();
EventManager.Instance.UpdateAvailable += OnUpdateAvailable;
EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested;
EventManager.Instance.DownloadsTeachingTipRequested += InstanceOnDownloadsTeachingTipRequested;
SetDefaultFonts();
Observable
.FromEventPattern<SizeChangedEventArgs>(this, nameof(SizeChanged))
.Where(x => x.EventArgs.PreviousSize != x.EventArgs.NewSize)
.Throttle(TimeSpan.FromMilliseconds(100))
.Select(x => x.EventArgs.NewSize)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(newSize =>
{
var validWindowPosition = Screens.All.Any(screen => screen.Bounds.Contains(Position));
settingsManager.Transaction(
s =>
{
s.WindowSettings = new WindowSettings(
newSize.Width,
newSize.Height,
validWindowPosition ? Position.X : 0,
validWindowPosition ? Position.Y : 0
);
},
ignoreMissingLibraryDir: true
);
});
Observable
.FromEventPattern<PixelPointEventArgs>(this, nameof(PositionChanged))
.Where(x => Screens.All.Any(screen => screen.Bounds.Contains(x.EventArgs.Point)))
.Throttle(TimeSpan.FromMilliseconds(100))
.Select(x => x.EventArgs.Point)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(position =>
{
settingsManager.Transaction(
s =>
{
s.WindowSettings = new WindowSettings(Width, Height, position.X, position.Y);
},
ignoreMissingLibraryDir: true
);
});
}
private void InstanceOnDownloadsTeachingTipRequested(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
if (
!settingsManager.Settings.SeenTeachingTips.Contains(
Core.Models.Settings.TeachingTip.DownloadsTip
)
)
{
var target = this.FindControl<NavigationViewItem>("FooterDownloadItem")!;
var tip = this.FindControl<TeachingTip>("DownloadsTeachingTip")!;
tip.Target = target;
tip.Subtitle = Languages.Resources.TeachingTip_DownloadsExplanation;
tip.IsOpen = true;
}
});
}
private void OnNavigateAndFindCivitModelRequested(object? sender, int e)
{
navigationService.NavigateTo<CheckpointBrowserViewModel>();
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
navigationService.SetFrame(FrameView ?? throw new NullReferenceException("Frame not found"));
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
Application.Current!.ActualThemeVariantChanged += OnActualThemeVariantChanged;
var theme = ActualThemeVariant;
// Enable mica for Windows 11
if (IsWindows11 && theme != FluentAvaloniaTheme.HighContrastTheme)
{
TryEnableMicaEffect();
}
}
protected override void OnClosing(WindowClosingEventArgs e)
{
// Show confirmation if package running
var runningPackageService = App.Services.GetRequiredService<RunningPackageService>();
if (
runningPackageService.RunningPackages.Count > 0
&& e.CloseReason is WindowCloseReason.WindowClosing
)
{
e.Cancel = true;
var dialog = CreateExitConfirmDialog();
Dispatcher
.UIThread.InvokeAsync(async () =>
{
if (
(TaskDialogStandardResult)await dialog.ShowAsync(true) == TaskDialogStandardResult.Yes
)
{
App.Services.GetRequiredService<MainWindow>().Hide();
App.Shutdown();
}
})
.SafeFireAndForget();
}
base.OnClosing(e);
}
private static TaskDialog CreateExitConfirmDialog()
{
var dialog = DialogHelper.CreateTaskDialog(
Languages.Resources.Label_ConfirmExit,
Languages.Resources.Label_ConfirmExitDetail
);
dialog.ShowProgressBar = false;
dialog.FooterVisibility = TaskDialogFooterVisibility.Never;
dialog.Buttons = new List<TaskDialogButton>
{
new("Exit", TaskDialogStandardResult.Yes),
TaskDialogButton.CancelButton
};
dialog.Buttons[0].IsDefault = true;
return dialog;
}
/// <inheritdoc />
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
App.Shutdown();
}
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;
}
if (DataContext is not MainWindowViewModel vm)
return;
// Navigate to first page
Dispatcher.UIThread.Post(
() =>
navigationService.NavigateTo(
vm.Pages[0],
new BetterSlideNavigationTransition
{
Effect = SlideNavigationTransitionEffect.FromBottom
}
)
);
// Check show update teaching tip
if (vm.UpdateViewModel.IsUpdateAvailable)
{
OnUpdateAvailable(this, vm.UpdateViewModel.UpdateInfo);
}
}
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 NavigationService_OnTypedNavigation(object? sender, TypedNavigationEventArgs e)
{
var mainViewModel = (MainWindowViewModel)DataContext!;
mainViewModel.SelectedCategory = mainViewModel
.Pages.Concat(mainViewModel.FooterPages)
.FirstOrDefault(x => x.GetType() == e.ViewModelType);
}
private void OnUpdateAvailable(object? sender, UpdateInfo? updateInfo)
{
Dispatcher.UIThread.Post(() =>
{
if (DataContext is MainWindowViewModel vm && vm.ShouldShowUpdateAvailableTeachingTip(updateInfo))
{
var target = this.FindControl<NavigationViewItem>("FooterUpdateItem")!;
var tip = this.FindControl<TeachingTip>("UpdateAvailableTeachingTip")!;
tip.Target = target;
tip.Subtitle = $"{Compat.AppVersion.ToDisplayString()} -> {updateInfo.Version}";
tip.IsOpen = true;
}
});
}
private void SetDefaultFonts()
{
if (App.Current is not null)
{
FontFamily = App.Current.GetPlatformDefaultFontFamily();
}
}
private void NavigationView_OnItemInvoked(object sender, NavigationViewItemInvokedEventArgs e)
{
if (e.InvokedItemContainer is NavigationViewItem nvi)
{
// Skip if this is the currently selected item
if (nvi.IsSelected)
{
return;
}
if (nvi.Tag is null)
{
throw new InvalidOperationException("NavigationViewItem Tag is null");
}
if (nvi.Tag is not ViewModelBase vm)
{
throw new InvalidOperationException(
$"NavigationViewItem Tag must be of type ViewModelBase, not {nvi.Tag?.GetType()}"
);
}
navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition());
}
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
if (IsWindows11)
{
if (ActualThemeVariant != FluentAvaloniaTheme.HighContrastTheme)
{
TryEnableMicaEffect();
}
else
{
ClearValue(BackgroundProperty);
ClearValue(TransparencyBackgroundFallbackProperty);
}
}
}
private void OnImageLoadFailed(object? sender, ImageLoadFailedEventArgs e)
{
var fileName = Path.GetFileName(e.Url);
var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName;
logger.LogWarning($"Could not load '{displayName}'\n({e.Exception.Message})");
}
private void TryEnableMicaEffect()
{
TransparencyBackgroundFallback = Brushes.Transparent;
TransparencyLevelHint = new[]
{
WindowTransparencyLevel.Mica,
WindowTransparencyLevel.AcrylicBlur,
WindowTransparencyLevel.Blur
};
if (ActualThemeVariant == ThemeVariant.Dark)
{
var color = this.TryFindResource("SolidBackgroundFillColorBase", ThemeVariant.Dark, out var value)
? (Color2)(Color)value!
: new Color2(30, 31, 34);
color = color.LightenPercent(-0.5f);
Background = new ImmutableSolidColorBrush(color, 0.72);
}
else if (ActualThemeVariant == ThemeVariant.Light)
{
// Similar effect here
var color = this.TryFindResource(
"SolidBackgroundFillColorBase",
ThemeVariant.Light,
out var value
)
? (Color2)(Color)value!
: new Color2(243, 243, 243);
color = color.LightenPercent(0.5f);
Background = new ImmutableSolidColorBrush(color, 0.9);
}
}
private void FooterDownloadItem_OnTapped(object? sender, TappedEventArgs e)
{
var item = sender as NavigationViewItem;
var flyout = item!.ContextFlyout;
flyout!.ShowAt(item);
progressFlyout = flyout;
}
private async void FooterUpdateItem_OnTapped(object? sender, TappedEventArgs e)
{
// show update window thing
if (DataContext is not MainWindowViewModel vm)
{
throw new NullReferenceException("DataContext is not MainWindowViewModel");
}
await vm.ShowUpdateDialog();
}
private void FooterDiscordItem_OnTapped(object? sender, TappedEventArgs e)
{
ProcessRunner.OpenUrl(Assets.DiscordServerUrl);
}
private void PatreonPatreonItem_OnTapped(object? sender, TappedEventArgs e)
{
ProcessRunner.OpenUrl(Assets.PatreonUrl);
}
private void TopLevel_OnBackRequested(object? sender, RoutedEventArgs e)
{
e.Handled = true;
navigationService.GoBack();
}
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
{
navigationService.GoBack();
}
}