[GUI] Add archive information and subdirectory panels with data models

This commit is contained in:
2026-04-18 12:05:11 +01:00
parent 016d992a78
commit 4705682fab
15 changed files with 3106 additions and 1890 deletions

View File

@@ -38,6 +38,7 @@ using System.Collections.Generic;
using System.Text;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Aaru.Localization;
namespace Aaru.CommonTypes.Interfaces;
@@ -46,28 +47,35 @@ namespace Aaru.CommonTypes.Interfaces;
public enum ArchiveSupportedFeature : uint
{
/// <summary>The archive supports filenames for its entries. If this flag is not set, files can only be accessed by number.</summary>
[LocalizedDescription(nameof(UI.Archive_supports_filenames))]
SupportsFilenames = 1 << 0,
/// <summary>
/// The archive supports compression. If this flag is not set, compressed and uncompressed lengths are always the
/// same.
/// </summary>
[LocalizedDescription(nameof(UI.Archive_supports_compression))]
SupportsCompression = 1 << 1,
/// <summary>
/// The archive supports subdirectories. If this flag is not set, all filenames are guaranteed to not contain any
/// "/" character.
/// </summary>
[LocalizedDescription(nameof(UI.Archive_supports_subdirectories))]
SupportsSubdirectories = 1 << 2,
/// <summary>
/// The archive supports explicit entries for directories (like Zip, for example). If this flag is not set,
/// directories are implicit by the relative name of the files.
/// </summary>
[LocalizedDescription(nameof(UI.Archive_has_explicit_directories))]
HasExplicitDirectories = 1 << 3,
/// <summary>The archive stores a timestamp with each entry if this flag is set.</summary>
[LocalizedDescription(nameof(UI.Archive_has_entry_timestamp))]
HasEntryTimestamp = 1 << 4,
/// <summary>If this flag is set, individual files or the whole archive might be encrypted or password-protected.</summary>
[LocalizedDescription(nameof(UI.Archive_supports_protection))]
SupportsProtection = 1 << 5, // TODO: not implemented yet
/// <summary>If this flag is set, the archive supports returning extended attributes (Xattrs) for each entry.</summary>
[LocalizedDescription(nameof(UI.Archive_supports_xattrs))]
SupportsXAttrs = 1 << 6
}

View File

@@ -0,0 +1,75 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ArchiveFileModel.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI data models.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains information about a file entry inside an archive.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using Aaru.CommonTypes.Structs;
using Avalonia.Media;
namespace Aaru.Gui.Models;
public sealed class ArchiveFileModel
{
public int EntryNumber { get; set; }
public string Name { get; set; }
public string Size => $"{Stat?.Length ?? 0}";
public string CreationTime =>
Stat is null || Stat.CreationTime == default(DateTime) ? "" : $"{Stat.CreationTime:G}";
public string LastAccessTime => Stat is null || Stat.AccessTime == default(DateTime) ? "" : $"{Stat.AccessTime:G}";
public string ChangedTime => Stat is null || Stat.StatusChangeTime == default(DateTime)
? ""
: $"{Stat.StatusChangeTime:G}";
public string LastBackupTime => Stat is null || Stat.BackupTime == default(DateTime) ? "" : $"{Stat.BackupTime:G}";
public string LastWriteTime => Stat is null || Stat.LastWriteTime == default(DateTime)
? ""
: $"{Stat.LastWriteTime:G}";
public string Attributes => Stat is null ? "" : $"{Stat.Attributes}";
public string Gid => Stat is null ? "" : $"{Stat.GID}";
public string Uid => Stat is null ? "" : $"{Stat.UID}";
public string Inode => Stat is null ? "" : $"{Stat.Inode}";
public string Links => Stat is null ? "" : $"{Stat.Links}";
public string Mode => Stat is null ? "" : $"{Stat.Mode}";
public FileEntryInfo Stat { get; set; }
public IBrush Color { get; set; }
}

View File

@@ -0,0 +1,49 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ArchiveModel.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI data models.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains information about an opened archive.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Collections.ObjectModel;
using Aaru.CommonTypes.Interfaces;
using Aaru.Gui.ViewModels.Panels;
using Avalonia.Media;
namespace Aaru.Gui.Models;
public sealed class ArchiveModel : RootModel
{
public string Path { get; set; }
public string FileName { get; set; }
public IImage Icon { get; set; }
public IArchive Archive { get; set; }
public IFilter Filter { get; set; }
public ArchiveInfoViewModel ViewModel { get; set; }
public ObservableCollection<ArchiveSubdirectoryModel> Roots { get; set; } = [];
}

View File

@@ -0,0 +1,47 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ArchiveSubdirectoryModel.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI data models.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains information about a virtual subdirectory inside an archive.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.Collections.ObjectModel;
using Aaru.CommonTypes.Interfaces;
using Avalonia.Media.Imaging;
namespace Aaru.Gui.Models;
public sealed class ArchiveSubdirectoryModel
{
public string Name { get; set; }
public string Path { get; set; }
public IArchive Archive { get; set; }
public ObservableCollection<ArchiveSubdirectoryModel> Subdirectories { get; set; } = [];
public ObservableCollection<ArchiveFileModel> Entries { get; set; } = [];
public Bitmap Icon { get; set; }
}

View File

@@ -0,0 +1,61 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ArchiveInfoViewModel.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI view models.
//
// --[ Description ] ----------------------------------------------------------
//
// View model and code for the opened archive information panel.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using Aaru.CommonTypes.Interfaces;
using Aaru.Localization;
using Humanizer;
namespace Aaru.Gui.ViewModels.Panels;
public sealed class ArchiveInfoViewModel
{
public ArchiveInfoViewModel(string path, IFilter filter, IArchive archive)
{
ArchivePathText = string.Format($"[blue]{path}[/]");
FilterText = string.Format(UI.Filter_0, filter.Name);
PluginText = string.Format(UI.Archive_plugin_0, archive.Name);
PluginIdText = string.Format(UI.Archive_plugin_id_0, archive.Id);
NumberOfEntriesText = string.Format(UI.Archive_number_of_entries_0, archive.NumberOfEntries);
FeaturesText = string.Format(UI.Archive_features_0, archive.ArchiveFeatures.Humanize());
archive.GetInformation(filter, null, out string information);
InformationText = information ?? "";
}
public string ArchivePathText { get; }
public string FilterText { get; }
public string PluginText { get; }
public string PluginIdText { get; }
public string NumberOfEntriesText { get; }
public string FeaturesText { get; }
public string InformationText { get; }
}

View File

@@ -0,0 +1,310 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ArchiveSubdirectoryViewModel.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI view models.
//
// --[ Description ] ----------------------------------------------------------
//
// View model and code for the archive subdirectory contents panel.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 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.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Interop;
using Aaru.Core;
using Aaru.Gui.Models;
using Aaru.Helpers;
using Aaru.Localization;
using Aaru.Logging;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.Input;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
using Sentry;
namespace Aaru.Gui.ViewModels.Panels;
public sealed class ArchiveSubdirectoryViewModel
{
const string MODULE_NAME = "Archive Subdirectory ViewModel";
readonly ArchiveSubdirectoryModel _model;
readonly Window _view;
public ArchiveSubdirectoryViewModel(ArchiveSubdirectoryModel model, Window view)
{
Entries = [];
SelectedEntries = [];
ExtractFilesCommand = new AsyncRelayCommand(ExtractFiles);
_model = model;
_view = view;
foreach(ArchiveFileModel entry in model.Entries)
{
entry.Color =
new SolidColorBrush(Color.Parse(DirColorsParser.Instance.ExtensionColors
.TryGetValue(Path.GetExtension(entry.Name),
out string hex)
? hex
: DirColorsParser.Instance.NormalColor));
Entries.Add(entry);
}
}
public ObservableCollection<ArchiveFileModel> Entries { get; }
public List<ArchiveFileModel> SelectedEntries { get; }
public ICommand ExtractFilesCommand { get; }
async Task ExtractFiles()
{
if(SelectedEntries.Count == 0) return;
IReadOnlyList<IStorageFolder> result =
await _view.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = UI.Dialog_Choose_destination_folder,
AllowMultiple = false
});
if(result.Count != 1) return;
Statistics.AddCommand("extract-files");
string folder = result[0].Path.AbsolutePath;
foreach(ArchiveFileModel file in SelectedEntries)
{
string filename = file.Name;
ButtonResult mboxResult;
if(DetectOS.IsWindows)
{
if(filename.Contains('<') ||
filename.Contains('>') ||
filename.Contains(':') ||
filename.Contains('\\') ||
filename.Contains('/') ||
filename.Contains('|') ||
filename.Contains('?') ||
filename.Contains('*') ||
filename.Any(static c => c < 32) ||
filename.Equals("CON", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("PRN", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("AUX", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM1", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM2", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM3", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM4", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM5", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM6", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM7", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM8", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("COM9", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT1", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT2", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT3", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT4", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT5", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT6", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT7", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT8", StringComparison.InvariantCultureIgnoreCase) ||
filename.Equals("LPT9", StringComparison.InvariantCultureIgnoreCase) ||
filename[^1] == '.' ||
filename[^1] == ' ')
{
char[] chars;
if(filename[^1] == '.' || filename[^1] == ' ')
chars = new char[filename.Length - 1];
else
chars = new char[filename.Length];
for(var ci = 0; ci < chars.Length; ci++)
{
chars[ci] = filename[ci] switch
{
'<'
or '>'
or ':'
or '\\'
or '/'
or '|'
or '?'
or '*'
or >= '\u0000' and <= '\u001F' => '_',
_ => filename[ci]
};
}
if(filename.StartsWith("CON", StringComparison.InvariantCultureIgnoreCase) ||
filename.StartsWith("PRN", StringComparison.InvariantCultureIgnoreCase) ||
filename.StartsWith("AUX", StringComparison.InvariantCultureIgnoreCase) ||
filename.StartsWith("COM", StringComparison.InvariantCultureIgnoreCase) ||
filename.StartsWith("LPT", StringComparison.InvariantCultureIgnoreCase))
{
chars[0] = '_';
chars[1] = '_';
chars[2] = '_';
}
string corrected = new(chars);
mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Unsupported_filename,
string
.Format(UI
.Filename_0_not_supported_want_to_rename_to_1,
filename,
corrected),
ButtonEnum.YesNoCancel,
Icon.Warning)
.ShowWindowDialogAsync(_view);
switch(mboxResult)
{
case ButtonResult.Cancel:
return;
case ButtonResult.No:
continue;
default:
filename = corrected;
break;
}
}
}
string outputPath = Path.Combine(folder, filename);
if(File.Exists(outputPath))
{
mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Existing_file,
string
.Format(UI.File_named_0_exists_overwrite_Q,
filename),
ButtonEnum.YesNoCancel,
Icon.Warning)
.ShowWindowDialogAsync(_view);
switch(mboxResult)
{
case ButtonResult.Cancel:
return;
case ButtonResult.No:
continue;
default:
try
{
File.Delete(outputPath);
}
catch(IOException)
{
mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Cannot_delete,
UI.Could_note_delete_existe_file_continue_Q,
ButtonEnum.YesNo,
Icon.Error)
.ShowWindowDialogAsync(_view);
if(mboxResult == ButtonResult.No) return;
}
break;
}
}
try
{
ErrorNumber error = _model.Archive.GetEntry(file.EntryNumber, out IFilter entryFilter);
if(error != ErrorNumber.NoError || entryFilter is null)
{
AaruLogging.Debug(MODULE_NAME, string.Format(UI.Skipped_non_regular_archive_entry_0, file.Name));
continue;
}
using(Stream inStream = entryFilter.GetDataForkStream())
{
await using(var fs = new FileStream(outputPath,
FileMode.CreateNew,
FileAccess.ReadWrite,
FileShare.None))
{
await inStream.CopyToAsync(fs);
}
}
var fi = new FileInfo(outputPath);
try
{
if(file.Stat?.CreationTimeUtc is not null) fi.CreationTimeUtc = file.Stat.CreationTimeUtc.Value;
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
}
try
{
if(file.Stat?.LastWriteTimeUtc is not null) fi.LastWriteTimeUtc = file.Stat.LastWriteTimeUtc.Value;
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
}
try
{
if(file.Stat?.AccessTimeUtc is not null) fi.LastAccessTimeUtc = file.Stat.AccessTimeUtc.Value;
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
}
}
catch(IOException)
{
mboxResult = await MessageBoxManager.GetMessageBoxStandard(UI.Cannot_create_file,
UI
.Could_not_create_destination_file_continue_Q,
ButtonEnum.YesNo,
Icon.Error)
.ShowWindowDialogAsync(_view);
if(mboxResult == ButtonResult.No) return;
}
}
}
}

View File

@@ -41,6 +41,7 @@ using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Interop;
using Aaru.CommonTypes.Structs;
using Aaru.Core;
using Aaru.Database;
using Aaru.Gui.Models;
@@ -67,6 +68,7 @@ using MsBox.Avalonia.Dto;
using MsBox.Avalonia.Enums;
using MsBox.Avalonia.Models;
using Spectre.Console;
using ArchiveInfo = Aaru.Gui.Views.Panels.ArchiveInfo;
using Console = Aaru.Gui.Views.Dialogs.Console;
using FileSystem = Aaru.CommonTypes.AaruMetadata.FileSystem;
using ImageInfo = Aaru.Gui.Views.Panels.ImageInfo;
@@ -83,7 +85,10 @@ public partial class MainWindowViewModel : ViewModelBase
readonly SvgImage _genericOpticalIcon;
readonly SvgImage _genericTapeIcon;
readonly Window _view;
Console _console;
ArchiveModel _archive;
[ObservableProperty]
bool _archiveLoaded;
Console _console;
[ObservableProperty]
[CanBeNull]
object _contentPanel;
@@ -107,13 +112,13 @@ public partial class MainWindowViewModel : ViewModelBase
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);
CalculateEntropyCommand = new RelayCommand(CalculateEntropy, () => ImageLoaded);
VerifyImageCommand = new RelayCommand(VerifyImage, () => ImageLoaded);
ChecksumImageCommand = new RelayCommand(ChecksumImage, () => ImageLoaded);
ConvertImageCommand = new RelayCommand(ConvertImage, () => ImageLoaded);
CreateSidecarCommand = new RelayCommand(CreateSidecar, () => ImageLoaded);
ViewImageSectorsCommand = new RelayCommand(ViewImageSectors, () => ImageLoaded);
DecodeImageMediaTagsCommand = new RelayCommand(DecodeImageMediaTags, () => ImageLoaded);
OpenMhddLogCommand = new AsyncRelayCommand(OpenMhddLogAsync);
OpenIbgLogCommand = new AsyncRelayCommand(OpenIbgLogAsync);
ConnectToRemoteCommand = new AsyncRelayCommand(ConnectToRemoteAsync);
@@ -240,6 +245,20 @@ public partial class MainWindowViewModel : ViewModelBase
DataContext = new SubdirectoryViewModel(subdirectoryModel, _view)
};
break;
case ArchiveModel archiveModel:
ContentPanel = new ArchiveInfo
{
DataContext = archiveModel.ViewModel
};
break;
case ArchiveSubdirectoryModel archiveSubdirectoryModel:
ContentPanel = new ArchiveSubdirectory
{
DataContext = new ArchiveSubdirectoryViewModel(archiveSubdirectoryModel, _view)
};
break;
}
}
@@ -388,7 +407,7 @@ public partial class MainWindowViewModel : ViewModelBase
// TODO: Extensions
IReadOnlyList<IStorageFile> result = await _view.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = UI.Dialog_Choose_image_to_open,
Title = UI.Dialog_Choose_file_to_open,
AllowMultiple = false,
FileTypeFilter = [FilePickerFileTypes.All]
});
@@ -417,12 +436,22 @@ public partial class MainWindowViewModel : ViewModelBase
// Detect the image format of the selected file
if(ImageFormat.Detect(inputFilter) is not IMediaImage imageFormat)
{
IMsBox<ButtonResult> msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error,
UI.Image_format_not_identified,
ButtonEnum.Ok,
Icon.Error);
// Try archive format as fallback
IArchive archiveFormat = ArchiveFormat.Detect(inputFilter);
await msbox.ShowAsync();
if(archiveFormat is null)
{
IMsBox<ButtonResult> msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error,
UI.Image_format_not_identified,
ButtonEnum.Ok,
Icon.Error);
await msbox.ShowAsync();
return;
}
await OpenArchiveAsync(archiveFormat, inputFilter, result[0].Path.LocalPath);
return;
}
@@ -666,6 +695,11 @@ public partial class MainWindowViewModel : ViewModelBase
Statistics.AddMedia(imageFormat.Info.MediaType, false);
Statistics.AddFilter(inputFilter.Name);
// Close any previously opened archive before replacing the tree
_archive?.Archive?.Close();
_archive = null;
ArchiveLoaded = false;
TreeRoot.Clear();
TreeRoot.Add(imageModel);
Title = $"Aaru - {imageModel.FileName}";
@@ -702,6 +736,144 @@ public partial class MainWindowViewModel : ViewModelBase
Statistics.AddCommand("image-info");
}
async Task OpenArchiveAsync(IArchive archiveFormat, IFilter inputFilter, string path)
{
AaruLogging.WriteLine(UI.Archive_format_identified_by_0_1, Markup.Escape(archiveFormat.Name), archiveFormat.Id);
try
{
ErrorNumber opened = archiveFormat.Open(inputFilter, null);
if(opened != ErrorNumber.NoError)
{
IMsBox<ButtonResult> msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error,
string.Format(UI.Error_0_opening_archive_format, opened),
ButtonEnum.Ok,
Icon.Error);
await msbox.ShowAsync();
AaruLogging.Error(UI.Unable_to_open_archive_format);
AaruLogging.Error(UI.No_error_given);
return;
}
var archiveModel = new ArchiveModel
{
Path = path,
FileName = Path.GetFileName(path),
Icon = _genericFolderIcon,
Archive = archiveFormat,
Filter = inputFilter,
ViewModel = new ArchiveInfoViewModel(path, inputFilter, archiveFormat)
};
var rootDir = new ArchiveSubdirectoryModel
{
Name = "/",
Path = "",
Archive = archiveFormat
};
bool supportsSubdirs =
archiveFormat.ArchiveFeatures.HasFlag(ArchiveSupportedFeature.SupportsSubdirectories);
for(var entryNumber = 0; entryNumber < archiveFormat.NumberOfEntries; entryNumber++)
{
ErrorNumber nameErr = archiveFormat.GetFilename(entryNumber, out string entryName);
if(nameErr != ErrorNumber.NoError || string.IsNullOrEmpty(entryName)) continue;
archiveFormat.Stat(entryNumber, out FileEntryInfo stat);
if(!supportsSubdirs || !entryName.Contains('/'))
{
rootDir.Entries.Add(new ArchiveFileModel
{
EntryNumber = entryNumber,
Name = entryName,
Stat = stat
});
continue;
}
string[] parts = entryName.Split('/');
ArchiveSubdirectoryModel current = rootDir;
for(var i = 0; i < parts.Length - 1; i++)
{
string part = parts[i];
if(string.IsNullOrEmpty(part)) continue;
ArchiveSubdirectoryModel child = current.Subdirectories.FirstOrDefault(s => s.Name == part);
if(child == null)
{
child = new ArchiveSubdirectoryModel
{
Name = part,
Path = string.Join('/', parts, 0, i + 1),
Archive = archiveFormat
};
current.Subdirectories.Add(child);
}
current = child;
}
// If the entry itself is a directory (trailing slash) skip adding as a file
string leaf = parts[^1];
if(string.IsNullOrEmpty(leaf))
{
// Entry represents a directory; ensure it's present as subdirectory
continue;
}
current.Entries.Add(new ArchiveFileModel
{
EntryNumber = entryNumber,
Name = leaf,
Stat = stat
});
}
archiveModel.Roots.Add(rootDir);
Statistics.AddFilter(inputFilter.Name);
// Close any previously opened archive before replacing the tree
_archive?.Archive?.Close();
TreeRoot.Clear();
TreeRoot.Add(archiveModel);
Title = $"Aaru - {archiveModel.FileName}";
_image = null;
ImageLoaded = false;
_archive = archiveModel;
ArchiveLoaded = true;
Statistics.AddCommand("archive-info");
}
catch(Exception ex)
{
IMsBox<ButtonResult> msbox = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error,
UI.Unable_to_open_archive_format,
ButtonEnum.Ok,
Icon.Error);
await msbox.ShowAsync();
AaruLogging.Error(UI.Unable_to_open_archive_format);
AaruLogging.Error(Aaru.Localization.Core.Error_0, ex.Message);
AaruLogging.Exception(ex, Aaru.Localization.Core.Error_0, ex.Message);
}
}
Task AboutAsync()
{
var dialog = new About();
@@ -757,8 +929,24 @@ public partial class MainWindowViewModel : ViewModelBase
return dialog.ShowDialog(_view);
}
internal void Exit() =>
internal void Exit()
{
_archive?.Archive?.Close();
_archive = null;
(Application.Current?.ApplicationLifetime as ClassicDesktopStyleApplicationLifetime)?.Shutdown();
}
partial void OnImageLoadedChanged(bool value)
{
(CalculateEntropyCommand as IRelayCommand)?.NotifyCanExecuteChanged();
(VerifyImageCommand as IRelayCommand)?.NotifyCanExecuteChanged();
(ChecksumImageCommand as IRelayCommand)?.NotifyCanExecuteChanged();
(ConvertImageCommand as IRelayCommand)?.NotifyCanExecuteChanged();
(CreateSidecarCommand as IRelayCommand)?.NotifyCanExecuteChanged();
(ViewImageSectorsCommand as IRelayCommand)?.NotifyCanExecuteChanged();
(DecodeImageMediaTagsCommand as IRelayCommand)?.NotifyCanExecuteChanged();
}
void Console()
{

View File

@@ -0,0 +1,82 @@
<!--
// /***************************************************************************
// Aaru Data Preservation Suite
//
//
// Filename : ArchiveInfo.xaml
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI panel.
//
// [ Description ]
//
// Archive information panel.
//
// [ 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 <http://www.gnu.org/licenses/>.
//
//
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
-->
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:panels="clr-namespace:Aaru.Gui.ViewModels.Panels"
xmlns:localization="clr-namespace:Aaru.Localization;assembly=Aaru.Localization"
xmlns:controls="clr-namespace:Aaru.Gui.Controls"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="panels:ArchiveInfoViewModel"
x:Class="Aaru.Gui.Views.Panels.ArchiveInfo">
<Grid RowDefinitions="Auto,*"
Margin="12"
RowSpacing="8">
<StackPanel Grid.Row="0"
Spacing="8">
<controls:SpectreTextBlock FontWeight="Bold"
Text="{x:Static localization:UI.Title_Archive_information}" />
<controls:SpectreTextBlock Text="{Binding ArchivePathText, Mode=OneWay}"
TextWrapping="Wrap" />
<controls:SpectreTextBlock Text="{Binding FilterText, Mode=OneWay}"
TextWrapping="Wrap" />
<controls:SpectreTextBlock Text="{Binding PluginText, Mode=OneWay}"
TextWrapping="Wrap" />
<controls:SpectreTextBlock Text="{Binding PluginIdText, Mode=OneWay}"
TextWrapping="Wrap" />
<controls:SpectreTextBlock Text="{Binding NumberOfEntriesText, Mode=OneWay}"
TextWrapping="Wrap" />
<controls:SpectreTextBlock Text="{Binding FeaturesText, Mode=OneWay}"
TextWrapping="Wrap" />
</StackPanel>
<Border Grid.Row="1"
BorderThickness="1"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
CornerRadius="8"
IsVisible="{Binding !!InformationText, Mode=OneWay}">
<StackPanel Spacing="8"
Margin="12">
<controls:SpectreTextBlock FontWeight="Bold"
Text="{x:Static localization:UI.Title_Details}" />
<ScrollViewer>
<controls:SpectreTextBlock Text="{Binding InformationText, Mode=OneWay}"
TextWrapping="Wrap" />
</ScrollViewer>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,43 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ArchiveInfo.xaml.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI panels.
//
// --[ Description ] ----------------------------------------------------------
//
// Archive information panel.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Aaru.Gui.Views.Panels;
public sealed class ArchiveInfo : UserControl
{
public ArchiveInfo() => InitializeComponent();
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}

View File

@@ -0,0 +1,108 @@
<!--
// /***************************************************************************
// Aaru Data Preservation Suite
//
//
// Filename : ArchiveSubdirectory.xaml
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI panel.
//
// [ Description ]
//
// Archive subdirectory contents panel.
//
// [ 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 <http://www.gnu.org/licenses/>.
//
//
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
-->
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:panels="clr-namespace:Aaru.Gui.ViewModels.Panels"
xmlns:localization="clr-namespace:Aaru.Localization;assembly=Aaru.Localization"
xmlns:controls="clr-namespace:Aaru.Gui.Controls"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="panels:ArchiveSubdirectoryViewModel"
x:Class="Aaru.Gui.Views.Panels.ArchiveSubdirectory">
<DataGrid Margin="12"
ItemsSource="{Binding Entries, Mode=OneWay}"
IsReadOnly="True"
SelectionMode="Extended">
<DataGrid.Styles>
<Style Selector="DataGridRow">
<Setter Property="Foreground"
Value="{ReflectionBinding Color, Mode=OneWay}" />
</Style>
</DataGrid.Styles>
<DataGrid.ContextMenu>
<ContextMenu>
<Button Command="{Binding ExtractFilesCommand, Mode=OneWay}">
<TextBlock Text="{x:Static localization:UI.ButtonLabel_Extract_to}" />
</Button>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<controls:SpectreTextBlock Text="{x:Static localization:UI.Title_Name}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Size, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<controls:SpectreTextBlock Text="{x:Static localization:UI.Title_Length}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding CreationTime, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<controls:SpectreTextBlock Text="{x:Static localization:UI.Title_Creation}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding LastAccessTime, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<controls:SpectreTextBlock Text="{x:Static localization:UI.Title_Last_access}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding LastWriteTime, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<controls:SpectreTextBlock Text="{x:Static localization:UI.Title_Last_write}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Attributes, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<controls:SpectreTextBlock Text="{x:Static localization:UI.Title_Attributes}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</UserControl>

View File

@@ -0,0 +1,43 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ArchiveSubdirectory.xaml.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : GUI panels.
//
// --[ Description ] ----------------------------------------------------------
//
// Archive subdirectory contents panel.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Aaru.Gui.Views.Panels;
public sealed class ArchiveSubdirectory : UserControl
{
public ArchiveSubdirectory() => InitializeComponent();
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}

View File

@@ -325,6 +325,28 @@
VerticalAlignment="Center" />
</StackPanel>
</TreeDataTemplate>
<TreeDataTemplate DataType="models:ArchiveModel"
ItemsSource="{Binding Roots, Mode=OneWay}">
<StackPanel Orientation="Horizontal"
Spacing="4">
<Image Width="24"
Height="24"
Source="{Binding Icon, Mode=OneWay}" />
<TextBlock Text="{Binding FileName, Mode=OneWay}"
VerticalAlignment="Center" />
</StackPanel>
</TreeDataTemplate>
<TreeDataTemplate DataType="models:ArchiveSubdirectoryModel"
ItemsSource="{Binding Subdirectories, Mode=OneWay}">
<StackPanel Orientation="Horizontal"
Spacing="4">
<Svg Width="24"
Height="24"
Path="/Assets/Icons/oxygen/inode-directory.svg" />
<TextBlock Text="{Binding Name, Mode=OneWay}"
VerticalAlignment="Center" />
</StackPanel>
</TreeDataTemplate>
<DataTemplate DataType="models:RootModel">
<TextBlock Text="{Binding Name, Mode=OneWay}" />
</DataTemplate>

View File

@@ -11255,5 +11255,95 @@ namespace Aaru.Localization {
return ResourceManager.GetString("Maximum_error_recovery_is_100", resourceCulture);
}
}
public static string Dialog_Choose_file_to_open {
get {
return ResourceManager.GetString("Dialog_Choose_file_to_open", resourceCulture);
}
}
public static string Error_0_opening_archive_format {
get {
return ResourceManager.GetString("Error_0_opening_archive_format", resourceCulture);
}
}
public static string Title_Archive_information {
get {
return ResourceManager.GetString("Title_Archive_information", resourceCulture);
}
}
public static string Archive_plugin_0 {
get {
return ResourceManager.GetString("Archive_plugin_0", resourceCulture);
}
}
public static string Archive_plugin_id_0 {
get {
return ResourceManager.GetString("Archive_plugin_id_0", resourceCulture);
}
}
public static string Archive_number_of_entries_0 {
get {
return ResourceManager.GetString("Archive_number_of_entries_0", resourceCulture);
}
}
public static string Archive_features_0 {
get {
return ResourceManager.GetString("Archive_features_0", resourceCulture);
}
}
public static string Skipped_non_regular_archive_entry_0 {
get {
return ResourceManager.GetString("Skipped_non_regular_archive_entry_0", resourceCulture);
}
}
public static string Archive_supports_filenames {
get {
return ResourceManager.GetString("Archive_supports_filenames", resourceCulture);
}
}
public static string Archive_supports_compression {
get {
return ResourceManager.GetString("Archive_supports_compression", resourceCulture);
}
}
public static string Archive_supports_subdirectories {
get {
return ResourceManager.GetString("Archive_supports_subdirectories", resourceCulture);
}
}
public static string Archive_has_explicit_directories {
get {
return ResourceManager.GetString("Archive_has_explicit_directories", resourceCulture);
}
}
public static string Archive_has_entry_timestamp {
get {
return ResourceManager.GetString("Archive_has_entry_timestamp", resourceCulture);
}
}
public static string Archive_supports_protection {
get {
return ResourceManager.GetString("Archive_supports_protection", resourceCulture);
}
}
public static string Archive_supports_xattrs {
get {
return ResourceManager.GetString("Archive_supports_xattrs", resourceCulture);
}
}
}
}

View File

@@ -5623,4 +5623,49 @@ Probadores:
<data name="Maximum_error_recovery_is_100" xml:space="preserve">
<value>La recuperación de errores máxima es 100%</value>
</data>
<data name="Dialog_Choose_file_to_open" xml:space="preserve">
<value>Elegir archivo a abrir</value>
</data>
<data name="Error_0_opening_archive_format" xml:space="preserve">
<value>Error {0} al abrir el formato de archivo.</value>
</data>
<data name="Title_Archive_information" xml:space="preserve">
<value>[bold][slateblue1]Información del archivo[/][/]</value>
</data>
<data name="Archive_plugin_0" xml:space="preserve">
<value>[slateblue1]Plugin:[/] [red]{0}[/]</value>
</data>
<data name="Archive_plugin_id_0" xml:space="preserve">
<value>[slateblue1]ID del plugin:[/] [purple]{0}[/]</value>
</data>
<data name="Archive_number_of_entries_0" xml:space="preserve">
<value>[slateblue1]Número de entradas:[/] [aqua]{0}[/]</value>
</data>
<data name="Archive_features_0" xml:space="preserve">
<value>[slateblue1]Características:[/] [orange3]{0}[/]</value>
</data>
<data name="Skipped_non_regular_archive_entry_0" xml:space="preserve">
<value>Se omitió la entrada de archivo no regular {0}.</value>
</data>
<data name="Archive_supports_filenames" xml:space="preserve">
<value>Soporta nombres de archivos</value>
</data>
<data name="Archive_supports_compression" xml:space="preserve">
<value>Soporta compresión</value>
</data>
<data name="Archive_supports_subdirectories" xml:space="preserve">
<value>Soporta subdirectorios</value>
</data>
<data name="Archive_has_explicit_directories" xml:space="preserve">
<value>Tiene subdirectories explícitos</value>
</data>
<data name="Archive_has_entry_timestamp" xml:space="preserve">
<value>Tiene marcas de tiempo</value>
</data>
<data name="Archive_supports_protection" xml:space="preserve">
<value>Soporta protección</value>
</data>
<data name="Archive_supports_xattrs" xml:space="preserve">
<value>Soporta atributos extendidos</value>
</data>
</root>

View File

@@ -5707,4 +5707,49 @@ Do you want to continue?</value>
<data name="Maximum_error_recovery_is_100" xml:space="preserve">
<value>Maximum error recovery is 100%</value>
</data>
<data name="Dialog_Choose_file_to_open" xml:space="preserve">
<value>Choose file to open</value>
</data>
<data name="Error_0_opening_archive_format" xml:space="preserve">
<value>Error {0} opening archive format.</value>
</data>
<data name="Title_Archive_information" xml:space="preserve">
<value>[bold][slateblue1]Archive information[/][/]</value>
</data>
<data name="Archive_plugin_0" xml:space="preserve">
<value>[slateblue1]Plugin:[/] [red]{0}[/]</value>
</data>
<data name="Archive_plugin_id_0" xml:space="preserve">
<value>[slateblue1]Plugin ID:[/] [purple]{0}[/]</value>
</data>
<data name="Archive_number_of_entries_0" xml:space="preserve">
<value>[slateblue1]Number of entries:[/] [aqua]{0}[/]</value>
</data>
<data name="Archive_features_0" xml:space="preserve">
<value>[slateblue1]Features:[/] [orange3]{0}[/]</value>
</data>
<data name="Skipped_non_regular_archive_entry_0" xml:space="preserve">
<value>Skipped non-regular archive entry {0}.</value>
</data>
<data name="Archive_supports_filenames" xml:space="preserve">
<value>Supports filenames</value>
</data>
<data name="Archive_supports_compression" xml:space="preserve">
<value>Supports compression</value>
</data>
<data name="Archive_supports_subdirectories" xml:space="preserve">
<value>Supports subdirectories</value>
</data>
<data name="Archive_has_explicit_directories" xml:space="preserve">
<value>Has explicit subdirectories</value>
</data>
<data name="Archive_has_entry_timestamp" xml:space="preserve">
<value>Has entry timestamp</value>
</data>
<data name="Archive_supports_protection" xml:space="preserve">
<value>Supports protection</value>
</data>
<data name="Archive_supports_xattrs" xml:space="preserve">
<value>Supports extended attributes</value>
</data>
</root>