using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.IO ;
using System.Linq ;
using System.Threading.Tasks ;
using Avalonia.Controls.Notifications ;
using Avalonia.Media.Imaging ;
using Avalonia.Platform.Storage ;
using Avalonia.Threading ;
using CommunityToolkit.Mvvm.ComponentModel ;
using FluentAvalonia.UI.Controls ;
using StabilityMatrix.Avalonia.Controls ;
using StabilityMatrix.Avalonia.Languages ;
using StabilityMatrix.Avalonia.Models ;
using StabilityMatrix.Avalonia.Services ;
using StabilityMatrix.Avalonia.ViewModels.Base ;
using StabilityMatrix.Core.Attributes ;
using StabilityMatrix.Core.Extensions ;
using StabilityMatrix.Core.Helper ;
using StabilityMatrix.Core.Models ;
using StabilityMatrix.Core.Models.Api ;
using StabilityMatrix.Core.Models.FileInterfaces ;
using StabilityMatrix.Core.Services ;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs ;
[ManagedService]
[Transient]
public partial class SelectModelVersionViewModel (
ISettingsManager settingsManager ,
IDownloadService downloadService ,
IModelIndexService modelIndexService ,
INotificationService notificationService
) : ContentDialogViewModelBase
{
private readonly IDownloadService downloadService = downloadService ;
public required ContentDialog Dialog { get ; set ; }
public required IReadOnlyList < ModelVersionViewModel > Versions { get ; set ; }
public required string Description { get ; set ; }
public required string Title { get ; set ; }
public required CivitModel CivitModel { get ; set ; }
[ObservableProperty]
private Bitmap ? previewImage ;
[ObservableProperty]
private ModelVersionViewModel ? selectedVersionViewModel ;
[ObservableProperty]
private CivitFileViewModel ? selectedFile ;
[ObservableProperty]
private bool isImportEnabled ;
[ObservableProperty]
private ObservableCollection < ImageSource > imageUrls = new ( ) ;
[ObservableProperty]
private bool canGoToNextImage ;
[ObservableProperty]
private bool canGoToPreviousImage ;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayedPageNumber))]
private int selectedImageIndex ;
[ObservableProperty]
private string importTooltip = string . Empty ;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCustomSelected))]
[NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))]
private string selectedInstallLocation = string . Empty ;
[ObservableProperty]
private ObservableCollection < string > availableInstallLocations = [ ] ;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))]
private string customInstallLocation = string . Empty ;
public bool IsCustomSelected = > SelectedInstallLocation = = "Custom..." ;
public bool ShowEmptyPathWarning = > IsCustomSelected & & string . IsNullOrWhiteSpace ( CustomInstallLocation ) ;
public int DisplayedPageNumber = > SelectedImageIndex + 1 ;
public override void OnLoaded ( )
{
SelectedVersionViewModel = Versions [ 0 ] ;
CanGoToNextImage = true ;
LoadInstallLocations ( ) ;
}
partial void OnSelectedVersionViewModelChanged ( ModelVersionViewModel ? value )
{
var nsfwEnabled = settingsManager . Settings . ModelBrowserNsfwEnabled ;
var allImages = value
? . ModelVersion
? . Images
? . Where ( img = > nsfwEnabled | | img . Nsfw = = "None" )
? . Select ( x = > new ImageSource ( x . Url ) )
. ToList ( ) ;
if ( allImages = = null | | ! allImages . Any ( ) )
{
allImages = new List < ImageSource > { new ( Assets . NoImage ) } ;
CanGoToNextImage = false ;
}
else
{
CanGoToNextImage = allImages . Count > 1 ;
}
Dispatcher . UIThread . Post ( ( ) = >
{
CanGoToPreviousImage = false ;
SelectedFile = SelectedVersionViewModel ? . CivitFileViewModels . FirstOrDefault ( ) ;
ImageUrls = new ObservableCollection < ImageSource > ( allImages ) ;
SelectedImageIndex = 0 ;
} ) ;
}
partial void OnSelectedFileChanged ( CivitFileViewModel ? value )
{
if ( value is { IsInstalled : true } ) { }
var canImport = true ;
if ( settingsManager . IsLibraryDirSet )
{
var fileSizeBytes = value ? . CivitFile . SizeKb * 1 0 2 4 ;
var freeSizeBytes =
SystemInfo . GetDiskFreeSpaceBytes ( settingsManager . ModelsDirectory ) ? ? long . MaxValue ;
canImport = fileSizeBytes < freeSizeBytes ;
ImportTooltip = canImport
? "Free space after download: "
+ (
freeSizeBytes < long . MaxValue
? Size . FormatBytes ( Convert . ToUInt64 ( freeSizeBytes - fileSizeBytes ) )
: "Unknown"
)
: $"Not enough space on disk. Need {Size.FormatBytes(Convert.ToUInt64(fileSizeBytes))} but only have {Size.FormatBytes(Convert.ToUInt64(freeSizeBytes))}" ;
}
else
{
ImportTooltip = "Please set the library directory in settings" ;
}
IsImportEnabled = value ? . CivitFile ! = null & & canImport & & ! ShowEmptyPathWarning ;
}
partial void OnSelectedInstallLocationChanged ( string value )
{
if ( value . Equals ( "Custom..." , StringComparison . OrdinalIgnoreCase ) )
{
Dispatcher . UIThread . InvokeAsync ( SelectCustomFolder ) ;
}
else
{
CustomInstallLocation = string . Empty ;
}
IsImportEnabled = ! ShowEmptyPathWarning ;
}
partial void OnCustomInstallLocationChanged ( string value )
{
IsImportEnabled = ! ShowEmptyPathWarning ;
}
public void Cancel ( )
{
Dialog . Hide ( ContentDialogResult . Secondary ) ;
}
public void Import ( )
{
Dialog . Hide ( ContentDialogResult . Primary ) ;
}
public async Task Delete ( )
{
if ( SelectedFile = = null )
return ;
var fileToDelete = SelectedFile ;
var originalSelectedVersionVm = SelectedVersionViewModel ;
var hash = fileToDelete . CivitFile . Hashes . BLAKE3 ;
if ( string . IsNullOrWhiteSpace ( hash ) )
{
notificationService . Show (
"Error deleting file" ,
"Could not delete model, hash is missing." ,
NotificationType . Error
) ;
return ;
}
var matchingModels = ( await modelIndexService . FindByHashAsync ( hash ) ) . ToList ( ) ;
if ( matchingModels . Count = = 0 )
{
await modelIndexService . RefreshIndex ( ) ;
matchingModels = ( await modelIndexService . FindByHashAsync ( hash ) ) . ToList ( ) ;
if ( matchingModels . Count = = 0 )
{
notificationService . Show (
"Error deleting file" ,
"Could not delete model, model not found in index." ,
NotificationType . Error
) ;
return ;
}
}
var dialog = new BetterContentDialog
{
Title = Resources . Label_AreYouSure ,
MaxDialogWidth = 7 5 0 ,
MaxDialogHeight = 8 5 0 ,
PrimaryButtonText = Resources . Action_Yes ,
IsPrimaryButtonEnabled = true ,
IsSecondaryButtonEnabled = false ,
CloseButtonText = Resources . Action_Cancel ,
DefaultButton = ContentDialogButton . Close ,
Content =
$"The following files:\n{string.Join('\n', matchingModels.Select(x => $" - { x . FileName } "))}\n"
+ "and all associated metadata files will be deleted. Are you sure?" ,
} ;
var result = await dialog . ShowAsync ( ) ;
if ( result = = ContentDialogResult . Primary )
{
foreach ( var localModel in matchingModels )
{
var checkpointPath = new FilePath ( localModel . GetFullPath ( settingsManager . ModelsDirectory ) ) ;
if ( File . Exists ( checkpointPath ) )
{
File . Delete ( checkpointPath ) ;
}
var previewPath = localModel . GetPreviewImageFullPath ( settingsManager . ModelsDirectory ) ;
if ( File . Exists ( previewPath ) )
{
File . Delete ( previewPath ) ;
}
var cmInfoPath = checkpointPath . ToString ( ) . Replace ( checkpointPath . Extension , ".cm-info.json" ) ;
if ( File . Exists ( cmInfoPath ) )
{
File . Delete ( cmInfoPath ) ;
}
await modelIndexService . RemoveModelAsync ( localModel ) ;
}
settingsManager . Transaction ( settings = > settings . InstalledModelHashes ? . Remove ( hash ) ) ;
fileToDelete . IsInstalled = false ;
originalSelectedVersionVm ? . RefreshInstallStatus ( ) ;
}
}
public void PreviousImage ( )
{
if ( SelectedImageIndex > 0 )
SelectedImageIndex - - ;
CanGoToPreviousImage = SelectedImageIndex > 0 ;
CanGoToNextImage = SelectedImageIndex < ImageUrls . Count - 1 ;
}
public void NextImage ( )
{
if ( SelectedImageIndex < ImageUrls . Count - 1 )
SelectedImageIndex + + ;
CanGoToPreviousImage = SelectedImageIndex > 0 ;
CanGoToNextImage = SelectedImageIndex < ImageUrls . Count - 1 ;
}
public async Task SelectCustomFolder ( )
{
var files = await App . StorageProvider . OpenFolderPickerAsync (
new FolderPickerOpenOptions
{
Title = "Select Download Folder" ,
AllowMultiple = false ,
SuggestedStartLocation = await App . StorageProvider . TryGetFolderFromPathAsync (
Path . Combine (
settingsManager . ModelsDirectory ,
CivitModel . Type . ConvertTo < SharedFolderType > ( ) . GetStringValue ( )
)
)
}
) ;
if ( files . FirstOrDefault ( ) ? . TryGetLocalPath ( ) is { } path )
{
CustomInstallLocation = path ;
}
}
private void LoadInstallLocations ( )
{
var rootModelsDirectory = new DirectoryPath ( settingsManager . ModelsDirectory ) ;
var downloadDirectory = rootModelsDirectory . JoinDir (
CivitModel . Type . ConvertTo < SharedFolderType > ( ) . GetStringValue ( )
) ;
var installLocations = new ObservableCollection < string >
{
downloadDirectory . ToString ( ) . Replace ( rootModelsDirectory , "Models" )
} ;
foreach ( var directory in downloadDirectory . EnumerateDirectories ( ) )
{
installLocations . Add ( directory . ToString ( ) . Replace ( rootModelsDirectory , "Models" ) ) ;
}
installLocations . Add ( "Custom..." ) ;
AvailableInstallLocations = installLocations ;
SelectedInstallLocation = installLocations . FirstOrDefault ( ) ;
}
}