diff --git a/Aaru.Gui/Models/FileSystemModel.cs b/Aaru.Gui/Models/FileSystemModel.cs index bed832cca..62cc459e0 100644 --- a/Aaru.Gui/Models/FileSystemModel.cs +++ b/Aaru.Gui/Models/FileSystemModel.cs @@ -33,6 +33,7 @@ using System.Collections.ObjectModel; using Aaru.CommonTypes.Interfaces; using Aaru.Gui.ViewModels.Panels; +using Avalonia.Media.Imaging; namespace Aaru.Gui.Models; @@ -45,4 +46,5 @@ public sealed class FileSystemModel : RootModel public IReadOnlyFilesystem ReadOnlyFilesystem { get; set; } public FileSystemViewModel ViewModel { get; set; } public ObservableCollection Roots { get; set; } + public Bitmap Icon { get; set; } } \ No newline at end of file diff --git a/Aaru.Gui/Models/ImageModel.cs b/Aaru.Gui/Models/ImageModel.cs index 517b76435..f982e189f 100644 --- a/Aaru.Gui/Models/ImageModel.cs +++ b/Aaru.Gui/Models/ImageModel.cs @@ -37,7 +37,7 @@ using Avalonia.Media.Imaging; namespace Aaru.Gui.Models; -public sealed class ImageModel +public sealed class ImageModel : RootModel { public ImageModel() => PartitionSchemesOrFileSystems = []; diff --git a/Aaru.Gui/Models/SubdirectoryModel.cs b/Aaru.Gui/Models/SubdirectoryModel.cs index 79fdf3894..26550aac2 100644 --- a/Aaru.Gui/Models/SubdirectoryModel.cs +++ b/Aaru.Gui/Models/SubdirectoryModel.cs @@ -32,6 +32,7 @@ using System.Collections.ObjectModel; using Aaru.CommonTypes.Interfaces; +using Avalonia.Media.Imaging; namespace Aaru.Gui.Models; @@ -44,4 +45,5 @@ public sealed class SubdirectoryModel public ObservableCollection Subdirectories { get; set; } public IReadOnlyFilesystem Plugin { get; set; } public bool Listed { get; set; } + public Bitmap Icon { get; set; } } \ No newline at end of file diff --git a/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs b/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs index 8d18e6856..5023273fd 100644 --- a/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs +++ b/Aaru.Gui/ViewModels/Windows/MainWindowViewModel.cs @@ -1,26 +1,48 @@ +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.AaruMetadata; +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.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 CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MsBox.Avalonia; +using MsBox.Avalonia.Base; +using MsBox.Avalonia.Enums; +using Spectre.Console; using Console = Aaru.Gui.Views.Dialogs.Console; +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] @@ -28,6 +50,8 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] bool _devicesSupported; [ObservableProperty] + string _title; + [ObservableProperty] ObservableCollection _treeRoot; object _treeViewSelectedItem; @@ -43,6 +67,19 @@ public partial class MainWindowViewModel : ViewModelBase ConsoleCommand = new RelayCommand(Console); OpenCommand = new AsyncRelayCommand(OpenAsync); + _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: @@ -62,6 +99,7 @@ public partial class MainWindowViewModel : ViewModelBase ]; _view = view; + Title = "Aaru"; } public ICommand OpenCommand { get; } @@ -90,10 +128,308 @@ public partial class MainWindowViewModel : ViewModelBase set => SetProperty(ref _treeViewSelectedItem, value); } - Task OpenAsync() => + 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] + }); - // TODO - null; + // 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}.png"); + + var imageModel = new ImageModel + { + Path = result[0].Path.LocalPath, + Icon = AssetLoader.Exists(mediaResource) + ? new Bitmap(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}"; + } + 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() { diff --git a/Aaru.Gui/Views/Windows/MainWindow.axaml b/Aaru.Gui/Views/Windows/MainWindow.axaml index 4db4d78b5..bf21a59ab 100644 --- a/Aaru.Gui/Views/Windows/MainWindow.axaml +++ b/Aaru.Gui/Views/Windows/MainWindow.axaml @@ -10,7 +10,7 @@ d:DesignHeight="450" x:Class="Aaru.Gui.Views.Windows.MainWindow" Icon="/Assets/aaru-logo.png" - Title="Aaru"> + Title="{Binding Title, Mode=OneWay}"> @@ -50,13 +50,78 @@ Command="{Binding AboutCommand, Mode=OneWay}" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +