// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : MainWindowViewModel.cs // Author(s) : Natalia Portillo // // Component : GUI view models. // // --[ Description ] ---------------------------------------------------------- // // Main window view model. // // --[ License ] -------------------------------------------------------------- // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General public License for more details. // // You should have received a copy of the GNU General public License // along with this program. If not, see . // // ---------------------------------------------------------------------------- // Copyright © 2011-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Interop; using Aaru.Core; using Aaru.Database; using Aaru.Gui.Models; using Aaru.Gui.ViewModels.Dialogs; using Aaru.Gui.ViewModels.Panels; using Aaru.Gui.Views.Dialogs; using Aaru.Gui.Views.Panels; using Aaru.Gui.Views.Windows; using Aaru.Localization; using Aaru.Logging; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Svg; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using JetBrains.Annotations; using MsBox.Avalonia; using MsBox.Avalonia.Base; using MsBox.Avalonia.Dto; using MsBox.Avalonia.Enums; using MsBox.Avalonia.Models; using Spectre.Console; using Console = Aaru.Gui.Views.Dialogs.Console; using FileSystem = Aaru.CommonTypes.AaruMetadata.FileSystem; using ImageInfo = Aaru.Gui.Views.Panels.ImageInfo; using Partition = Aaru.CommonTypes.Partition; using PlatformID = Aaru.CommonTypes.Interop.PlatformID; namespace Aaru.Gui.ViewModels.Windows; public partial class MainWindowViewModel : ViewModelBase { const string MODULE_NAME = "Main Window ViewModel"; readonly Bitmap _genericFolderIcon; readonly Bitmap _genericHddIcon; readonly Bitmap _genericOpticalIcon; readonly Bitmap _genericTapeIcon; readonly Window _view; Console _console; [ObservableProperty] [CanBeNull] object _contentPanel; [ObservableProperty] bool _devicesSupported; ImageModel _image; [ObservableProperty] bool _imageLoaded; [ObservableProperty] string _title; [ObservableProperty] ObservableCollection _treeRoot; public MainWindowViewModel(Window view) { AboutCommand = new AsyncRelayCommand(AboutAsync); EncodingsCommand = new AsyncRelayCommand(EncodingsAsync); PluginsCommand = new AsyncRelayCommand(PluginsAsync); StatisticsCommand = new AsyncRelayCommand(StatisticsAsync); ExitCommand = new RelayCommand(Exit); SettingsCommand = new AsyncRelayCommand(SettingsAsync); ConsoleCommand = new RelayCommand(Console); OpenCommand = new AsyncRelayCommand(OpenAsync); CalculateEntropyCommand = new RelayCommand(CalculateEntropy); VerifyImageCommand = new RelayCommand(VerifyImage); ChecksumImageCommand = new RelayCommand(ChecksumImage); ConvertImageCommand = new RelayCommand(ConvertImage); CreateSidecarCommand = new RelayCommand(CreateSidecar); ViewImageSectorsCommand = new RelayCommand(ViewImageSectors); DecodeImageMediaTagsCommand = new RelayCommand(DecodeImageMediaTags); OpenMhddLogCommand = new AsyncRelayCommand(OpenMhddLogAsync); OpenIbgLogCommand = new AsyncRelayCommand(OpenIbgLogAsync); ConnectToRemoteCommand = new AsyncRelayCommand(ConnectToRemoteAsync); OpenDeviceCommand = new RelayCommand(OpenDevice); _genericHddIcon = new Bitmap(AssetLoader.Open(new Uri("avares://Aaru.Gui/Assets/Icons/oxygen/32x32/drive-harddisk.png"))); _genericOpticalIcon = new Bitmap(AssetLoader.Open(new Uri("avares://Aaru.Gui/Assets/Icons/oxygen/32x32/drive-optical.png"))); _genericTapeIcon = new Bitmap(AssetLoader.Open(new Uri("avares://Aaru.Gui/Assets/Icons/oxygen/32x32/media-tape.png"))); _genericFolderIcon = new Bitmap(AssetLoader.Open(new Uri("avares://Aaru.Gui/Assets/Icons/oxygen/32x32/inode-directory.png"))); switch(DetectOS.GetRealPlatformID()) { case PlatformID.Win32NT: case PlatformID.Linux: DevicesSupported = true; break; } TreeRoot = [ new RootModel { Name = UI.Nothing_opened } ]; _view = view; Title = "Aaru"; } public ICommand OpenCommand { get; } public ICommand SettingsCommand { get; } public ICommand ExitCommand { get; } public ICommand ConsoleCommand { get; } public ICommand EncodingsCommand { get; } public ICommand PluginsCommand { get; } public ICommand StatisticsCommand { get; } public ICommand AboutCommand { get; } public ICommand CalculateEntropyCommand { get; } public ICommand VerifyImageCommand { get; } public ICommand ChecksumImageCommand { get; } public ICommand ConvertImageCommand { get; } public ICommand CreateSidecarCommand { get; } public ICommand ViewImageSectorsCommand { get; } public ICommand DecodeImageMediaTagsCommand { get; } public ICommand OpenMhddLogCommand { get; } public ICommand OpenIbgLogCommand { get; } public ICommand ConnectToRemoteCommand { get; } public ICommand OpenDeviceCommand { get; } public bool NativeMenuSupported { get { Window mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime) ?.MainWindow; return mainWindow is not null && NativeMenu.GetIsNativeMenuExported(mainWindow); } } public object TreeViewSelectedItem { get; set { if(value == field) return; SetProperty(ref field, value); ContentPanel = null; switch(value) { case ImageModel imageModel: ContentPanel = new ImageInfo { DataContext = imageModel.ViewModel }; break; case PartitionModel partitionModel: ContentPanel = new Views.Panels.Partition { DataContext = partitionModel.ViewModel }; break; case FileSystemModel fileSystemModel: ContentPanel = new Views.Panels.FileSystem { DataContext = fileSystemModel.ViewModel }; break; case SubdirectoryModel subdirectoryModel: ContentPanel = new Subdirectory { DataContext = new SubdirectoryViewModel(subdirectoryModel, _view) }; break; } } } void OpenDevice() { var deviceListWindow = new DeviceList(); deviceListWindow.DataContext = new DeviceListViewModel(deviceListWindow); deviceListWindow.Show(); } async Task ConnectToRemoteAsync() { IMsBox msbox = MessageBoxManager.GetMessageBoxCustom(new MessageBoxCustomParams { ContentTitle = UI.Connect_to_AaruRemote, ButtonDefinitions = [ new ButtonDefinition { Name = UI.ButtonLabel_Connect, IsDefault = true }, new ButtonDefinition { Name = UI.ButtonLabel_Cancel, IsCancel = true } ], CanResize = false, CloseOnClickAway = false, InputParams = new InputParams { Label = UI.Address_IP_or_hostname, DefaultValue = "", Multiline = false }, ContentMessage = UI.Introduce_AaruRemote_server_address, MinWidth = 400 }); string result = await msbox.ShowWindowDialogAsync(_view); if(result != UI.ButtonLabel_Connect) return; var deviceListWindow = new DeviceList { WindowStartupLocation = WindowStartupLocation.CenterScreen }; deviceListWindow.DataContext = new DeviceListViewModel(deviceListWindow, msbox.InputValue); deviceListWindow.Show(); } async Task OpenMhddLogAsync() { IReadOnlyList result = await _view.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = UI.Choose_MHDD_log_to_open, AllowMultiple = false, FileTypeFilter = [FilePickerFileTypes.MhddLogFiles] }); // Exit if user did not select exactly one file if(result.Count != 1) return; var mhddLogViewWindow = new MhddLogView(); mhddLogViewWindow.DataContext = new MhddLogViewModel(mhddLogViewWindow, result[0].Path.LocalPath); mhddLogViewWindow.Show(); } async Task OpenIbgLogAsync() { IReadOnlyList result = await _view.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = UI.Choose_IMGBurn_log_to_open, AllowMultiple = false, FileTypeFilter = [FilePickerFileTypes.IbgLogFiles] }); // Exit if user did not select exactly one file if(result.Count != 1) return; var ibgLogViewWindow = new IbgLogView(); ibgLogViewWindow.DataContext = new IbgLogViewModel(ibgLogViewWindow, result[0].Path.LocalPath); ibgLogViewWindow.Show(); } async Task OpenAsync() { // Open file picker dialog to allow user to select an image file // TODO: Extensions IReadOnlyList result = await _view.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = UI.Dialog_Choose_image_to_open, AllowMultiple = false, FileTypeFilter = [FilePickerFileTypes.All] }); // Exit if user did not select exactly one file if(result.Count != 1) return; // Get the appropriate filter plugin for the selected file IFilter inputFilter = PluginRegister.Singleton.GetFilter(result[0].Path.LocalPath); // Show error if no suitable filter plugin is found if(inputFilter == null) { IMsBox msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, UI.Cannot_open_specified_file, ButtonEnum.Ok, Icon.Error); await msbox.ShowAsync(); return; } try { // Detect the image format of the selected file if(ImageFormat.Detect(inputFilter) is not IMediaImage imageFormat) { IMsBox msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, UI.Image_format_not_identified, ButtonEnum.Ok, Icon.Error); await msbox.ShowAsync(); return; } AaruLogging.WriteLine(UI.Image_format_identified_by_0_1, Markup.Escape(imageFormat.Name), imageFormat.Id); try { // Open the image file ErrorNumber opened = imageFormat.Open(inputFilter); if(opened != ErrorNumber.NoError) { IMsBox msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, string.Format(UI.Error_0_opening_image_format, opened), ButtonEnum.Ok, Icon.Error); await msbox.ShowAsync(); AaruLogging.Error(UI.Unable_to_open_image_format); AaruLogging.Error(UI.No_error_given); return; } // Create image model with appropriate icon based on media type var mediaResource = new Uri($"avares://Aaru.Gui/Assets/Logos/Media/{imageFormat.Info.MediaType}.svg"); var imageModel = new ImageModel { Path = result[0].Path.LocalPath, Icon = AssetLoader.Exists(mediaResource) ? new SvgImage { Source = SvgSource.Load(AssetLoader.Open(mediaResource)) } : imageFormat.Info.MetadataMediaType == MetadataMediaType.BlockMedia ? _genericHddIcon : imageFormat.Info.MetadataMediaType == MetadataMediaType.OpticalDisc ? _genericOpticalIcon : _genericFolderIcon, FileName = Path.GetFileName(result[0].Path.LocalPath), Image = imageFormat, ViewModel = new ImageInfoViewModel(result[0].Path.LocalPath, inputFilter, imageFormat, _view), Filter = inputFilter }; // Extract all partitions from the image List partitions = Core.Partitions.GetAll(imageFormat); Core.Partitions.AddSchemesToStats(partitions); var checkRaw = false; List idPlugins; PluginRegister plugins = PluginRegister.Singleton; // Process partitions or raw device if no partitions found if(partitions.Count == 0) { AaruLogging.Debug(MODULE_NAME, UI.No_partitions_found); checkRaw = true; } else { AaruLogging.WriteLine(UI._0_partitions_found, partitions.Count); // Group partitions by scheme and process each one foreach(string scheme in partitions.Select(static p => p.Scheme).Distinct().Order()) { // TODO: Add icons to partition schemes var schemeModel = new PartitionSchemeModel { Name = scheme }; foreach(Partition partition in partitions.Where(p => p.Scheme == scheme) .OrderBy(static p => p.Start)) { var partitionModel = new PartitionModel { // TODO: Add icons to partition types Name = $"{partition.Name} ({partition.Type})", Partition = partition, ViewModel = new PartitionViewModel(partition) }; AaruLogging.WriteLine(UI.Identifying_filesystems_on_partition); // Identify all filesystems on this partition Core.Filesystems.Identify(imageFormat, out idPlugins, partition); if(idPlugins.Count == 0) AaruLogging.WriteLine(UI.Filesystem_not_identified); else { AaruLogging.WriteLine(string.Format(UI.Identified_by_0_plugins, idPlugins.Count)); // Mount and create models for each identified filesystem foreach(string pluginName in idPlugins) { if(!plugins.Filesystems.TryGetValue(pluginName, out IFilesystem fs)) continue; if(fs is null) continue; fs.GetInformation(imageFormat, partition, null, out string information, out FileSystem fsMetadata); var rofs = fs as IReadOnlyFilesystem; if(rofs != null) { ErrorNumber error = rofs.Mount(imageFormat, partition, null, [], null); if(error != ErrorNumber.NoError) rofs = null; } var filesystemModel = new FileSystemModel { VolumeName = rofs?.Metadata.VolumeName is null ? fsMetadata.VolumeName is null ? fsMetadata.Type : $"{fsMetadata.VolumeName} ({fsMetadata.Type})" : $"{rofs.Metadata.VolumeName} ({rofs.Metadata.Type})", Filesystem = fs, ReadOnlyFilesystem = rofs, ViewModel = new FileSystemViewModel(rofs?.Metadata ?? fsMetadata, information) }; // TODO: Trap expanding item if(rofs != null) { filesystemModel.Roots.Add(new SubdirectoryModel { Name = "/", Path = "", Plugin = rofs }); Statistics.AddCommand("ls"); } Statistics.AddFilesystem(rofs?.Metadata.Type ?? fsMetadata.Type); partitionModel.FileSystems.Add(filesystemModel); } } schemeModel.Partitions.Add(partitionModel); } imageModel.PartitionSchemesOrFileSystems.Add(schemeModel); } } // If no partitions were found, check the raw device if(checkRaw) { var wholePart = new Partition { Name = Localization.Core.Whole_device, Length = imageFormat.Info.Sectors, Size = imageFormat.Info.Sectors * imageFormat.Info.SectorSize }; Core.Filesystems.Identify(imageFormat, out idPlugins, wholePart); if(idPlugins.Count == 0) AaruLogging.WriteLine(UI.Filesystem_not_identified); else { AaruLogging.WriteLine(string.Format(UI.Identified_by_0_plugins, idPlugins.Count)); // Mount and create models for each identified filesystem on raw device foreach(string pluginName in idPlugins) { if(!plugins.Filesystems.TryGetValue(pluginName, out IFilesystem fs)) continue; if(fs is null) continue; fs.GetInformation(imageFormat, wholePart, null, out string information, out FileSystem fsMetadata); var rofs = fs as IReadOnlyFilesystem; if(rofs != null) { ErrorNumber error = rofs.Mount(imageFormat, wholePart, null, [], null); if(error != ErrorNumber.NoError) rofs = null; } var filesystemModel = new FileSystemModel { VolumeName = rofs?.Metadata.VolumeName is null ? fsMetadata.VolumeName is null ? fsMetadata.Type : $"{fsMetadata.VolumeName} ({fsMetadata.Type})" : $"{rofs.Metadata.VolumeName} ({rofs.Metadata.Type})", Filesystem = fs, ReadOnlyFilesystem = rofs, ViewModel = new FileSystemViewModel(rofs?.Metadata ?? fsMetadata, information) }; // TODO: Trap expanding item if(rofs != null) { filesystemModel.Roots.Add(new SubdirectoryModel { Name = "/", Path = "", Plugin = rofs }); Statistics.AddCommand("ls"); } Statistics.AddFilesystem(rofs?.Metadata.Type ?? fsMetadata.Type); imageModel.PartitionSchemesOrFileSystems.Add(filesystemModel); } } } // Update statistics and populate the tree view with the opened image Statistics.AddMediaFormat(imageFormat.Format); Statistics.AddMedia(imageFormat.Info.MediaType, false); Statistics.AddFilter(inputFilter.Name); TreeRoot.Clear(); TreeRoot.Add(imageModel); Title = $"Aaru - {imageModel.FileName}"; ImageLoaded = true; _image = imageModel; } catch(Exception ex) { IMsBox msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, UI.Unable_to_open_image_format, ButtonEnum.Ok, Icon.Error); await msbox.ShowAsync(); AaruLogging.Error(UI.Unable_to_open_image_format); AaruLogging.Error(Localization.Core.Error_0, ex.Message); AaruLogging.Exception(ex, Localization.Core.Error_0, ex.Message); } } catch(Exception ex) { IMsBox msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, UI.Exception_reading_file, ButtonEnum.Ok, Icon.Error); await msbox.ShowAsync(); AaruLogging.Error(string.Format(UI.Error_reading_file_0, ex.Message)); AaruLogging.Exception(ex, UI.Error_reading_file_0, ex.Message); } Statistics.AddCommand("image-info"); } Task AboutAsync() { var dialog = new About(); dialog.DataContext = new AboutViewModel(dialog); return dialog.ShowDialog(_view); } Task EncodingsAsync() { var dialog = new Encodings(); dialog.DataContext = new EncodingsViewModel(dialog); return dialog.ShowDialog(_view); } Task PluginsAsync() { var dialog = new PluginsDialog(); dialog.DataContext = new PluginsViewModel(dialog); return dialog.ShowDialog(_view); } async Task StatisticsAsync() { await using var ctx = AaruContext.Create(Settings.Settings.LocalDbPath); if(!ctx.Commands.Any() && !ctx.Filesystems.Any() && !ctx.Filters.Any() && !ctx.MediaFormats.Any() && !ctx.Medias.Any() && !ctx.Partitions.Any() && !ctx.SeenDevices.Any()) { await MessageBoxManager.GetMessageBoxStandard(UI.Title_Warning, UI.There_are_no_statistics) .ShowWindowDialogAsync(_view); return; } var dialog = new StatisticsDialog(); dialog.DataContext = new StatisticsViewModel(dialog); await dialog.ShowDialog(_view); } internal Task SettingsAsync() { var dialog = new SettingsDialog(); dialog.DataContext = new SettingsViewModel(dialog, false); return dialog.ShowDialog(_view); } internal void Exit() => (Application.Current?.ApplicationLifetime as ClassicDesktopStyleApplicationLifetime)?.Shutdown(); void Console() { if(_console is null) { _console = new Console(); _console.DataContext = new ConsoleViewModel(_console); } _console.Show(); } void CalculateEntropy() { if(_image is not {} imageModel) return; var imageEntropyWindow = new ImageEntropy(); imageEntropyWindow.DataContext = new ImageEntropyViewModel(imageModel.Image, imageEntropyWindow); imageEntropyWindow.Closed += (_, _) => imageEntropyWindow = null; imageEntropyWindow.Show(); } void VerifyImage() { if(_image is not {} imageModel) return; var imageVerifyWindow = new ImageVerify(); imageVerifyWindow.DataContext = new ImageVerifyViewModel(imageModel.Image, imageVerifyWindow); imageVerifyWindow.Closed += (_, _) => imageVerifyWindow = null; imageVerifyWindow.Show(); } void ChecksumImage() { if(_image is not {} imageModel) return; var imageChecksumWindow = new ImageChecksum(); imageChecksumWindow.DataContext = new ImageChecksumViewModel(imageModel.Image, imageChecksumWindow); imageChecksumWindow.Closed += (_, _) => imageChecksumWindow = null; imageChecksumWindow.Show(); } void ConvertImage() { if(_image is not {} imageModel) return; var imageConvertWindow = new ImageConvert(); imageConvertWindow.DataContext = new ImageConvertViewModel(imageModel.Image, imageModel.Path, imageConvertWindow); imageConvertWindow.Closed += (_, _) => imageConvertWindow = null; imageConvertWindow.Show(); } void CreateSidecar() { if(_image is not {} imageModel) return; var imageSidecarWindow = new ImageSidecar(); // TODO: Pass thru chosen default encoding imageSidecarWindow.DataContext = new ImageSidecarViewModel(imageModel.Image, imageModel.Path, imageModel.Filter.Id, null, imageSidecarWindow); imageSidecarWindow.Show(); } void ViewImageSectors() { if(_image is not {} imageModel) return; new ViewSector { DataContext = new ViewSectorViewModel(imageModel.Image) }.Show(); } void DecodeImageMediaTags() { if(_image is not {} imageModel) return; new DecodeMediaTags { DataContext = new DecodeMediaTagsViewModel(imageModel.Image) }.Show(); } }