Files
Aaru/Aaru.Tui/ViewModels/Windows/MainWindowViewModel.cs

408 lines
17 KiB
C#
Raw Normal View History

2025-10-16 16:39:31 +01:00
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Text User Interface.
//
// --[ 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-2025 Natalia Portillo
// ****************************************************************************/
2025-10-15 21:25:05 +01:00
using System.Collections.ObjectModel;
using System.Reflection;
2025-10-16 01:15:13 +01:00
using System.Text;
2025-10-15 21:25:05 +01:00
using System.Windows.Input;
2025-10-16 01:15:13 +01:00
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Core;
2025-10-15 22:05:55 +01:00
using Aaru.Helpers;
using Aaru.Tui.Models;
using Aaru.Tui.ViewModels.Dialogs;
using Aaru.Tui.Views.Dialogs;
2025-10-16 11:03:35 +01:00
using Aaru.Tui.Views.Windows;
2025-10-15 21:25:05 +01:00
using Avalonia;
2025-10-16 11:57:23 +01:00
using Avalonia.Controls;
2025-10-15 21:25:05 +01:00
using Avalonia.Controls.ApplicationLifetimes;
2025-10-15 22:05:55 +01:00
using Avalonia.Media;
2025-10-15 21:25:05 +01:00
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
2025-10-16 01:15:13 +01:00
using Humanizer;
using Humanizer.Bytes;
2025-10-15 22:05:55 +01:00
using Color = Avalonia.Media.Color;
2025-10-15 21:25:05 +01:00
namespace Aaru.Tui.ViewModels.Windows;
2025-10-15 22:05:55 +01:00
public sealed partial class MainWindowViewModel : ViewModelBase
2025-10-15 21:25:05 +01:00
{
2025-10-16 11:57:23 +01:00
readonly Window _view;
2025-10-15 21:25:05 +01:00
[ObservableProperty]
2025-10-16 00:13:02 +01:00
string _copyright;
2025-10-15 22:05:55 +01:00
[ObservableProperty]
2025-10-16 00:13:02 +01:00
string _currentPath;
2025-10-15 22:44:09 +01:00
[ObservableProperty]
2025-10-15 22:05:55 +01:00
ObservableCollection<FileModel> _files = [];
2025-10-15 21:25:05 +01:00
[ObservableProperty]
2025-10-16 00:13:02 +01:00
string _informationalVersion;
2025-10-16 01:23:17 +01:00
[ObservableProperty]
bool _isStatusVisible;
2025-10-16 00:13:02 +01:00
FileModel? _selectedFile;
2025-10-16 01:23:17 +01:00
[ObservableProperty]
string _status;
2025-10-15 21:25:05 +01:00
2025-10-16 11:57:23 +01:00
public MainWindowViewModel(Window view)
2025-10-15 21:25:05 +01:00
{
2025-10-16 11:57:23 +01:00
_view = view;
2025-10-15 22:54:35 +01:00
ExitCommand = new RelayCommand(Exit);
SectorViewCommand = new RelayCommand(SectorView);
GoToPathCommand = new AsyncRelayCommand(GoToPathAsync);
HelpCommand = new AsyncRelayCommand(HelpAsync);
2025-10-15 22:54:35 +01:00
OpenSelectedFileCommand = new RelayCommand(OpenSelectedFile, CanOpenSelectedFile);
2025-10-15 21:25:05 +01:00
InformationalVersion =
Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ??
"?.?.?";
2025-10-15 22:05:55 +01:00
Copyright = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyCopyrightAttribute>()?.Copyright ?? "";
2025-10-16 01:23:17 +01:00
Status = "Loading...";
2025-10-15 21:25:05 +01:00
}
2025-10-16 00:13:02 +01:00
public FileModel? SelectedFile
{
get => _selectedFile;
set
{
SetProperty(ref _selectedFile, value);
OnPropertyChanged(nameof(IsFileInfoAvailable));
OnPropertyChanged(nameof(SelectedFileIsNotDirectory));
OnPropertyChanged(nameof(SelectedFileLength));
OnPropertyChanged(nameof(SelectedFileCreationTime));
OnPropertyChanged(nameof(SelectedFileLastWriteTime));
OnPropertyChanged(nameof(SelectedFileAttributes));
OnPropertyChanged(nameof(SelectedFileUnixMode));
2025-10-16 01:15:13 +01:00
OnPropertyChanged(nameof(SelectedFileHasInformation));
OnPropertyChanged(nameof(SelectedFileInformation));
2025-10-16 00:13:02 +01:00
}
}
2025-10-15 22:54:35 +01:00
2025-10-16 00:13:02 +01:00
public ICommand OpenSelectedFileCommand { get; }
public ICommand ExitCommand { get; }
public ICommand SectorViewCommand { get; }
public ICommand GoToPathCommand { get; }
public ICommand HelpCommand { get; }
2025-10-16 00:13:02 +01:00
public bool IsFileInfoAvailable => SelectedFile?.FileInfo != null;
public bool SelectedFileIsNotDirectory => SelectedFile?.IsDirectory == false;
public long? SelectedFileLength => SelectedFile?.IsDirectory == false ? SelectedFile?.FileInfo?.Length : 0;
public DateTime? SelectedFileCreationTime => SelectedFile?.FileInfo?.CreationTime;
public DateTime? SelectedFileLastWriteTime => SelectedFile?.FileInfo?.LastWriteTime;
public string? SelectedFileAttributes => SelectedFile?.FileInfo?.Attributes.ToString();
public string? SelectedFileUnixMode => SelectedFile?.FileInfo?.UnixFileMode.ToString();
2025-10-16 01:15:13 +01:00
public bool SelectedFileHasInformation => SelectedFile?.Information != null;
public string? SelectedFileInformation => SelectedFile?.Information;
2025-10-15 21:25:05 +01:00
Task HelpAsync()
{
var dialog = new MainHelpDialog
{
DataContext = new MainHelpDialogViewModel(null!)
};
// Set the dialog reference after creation
((MainHelpDialogViewModel)dialog.DataContext!)._dialog = dialog;
return dialog.ShowDialog(_view);
}
async Task GoToPathAsync()
{
var dialog = new GoToPathDialog
{
DataContext = new GoToPathDialogViewModel(null!)
};
// Set the dialog reference after creation
((GoToPathDialogViewModel)dialog.DataContext!)._dialog = dialog;
bool? result = await dialog.ShowDialog<bool?>(_view);
if(result == true)
{
var viewModel = (GoToPathDialogViewModel)dialog.DataContext;
if(viewModel.Path is not null && Directory.Exists(viewModel.Path))
{
Environment.CurrentDirectory = viewModel.Path;
LoadFiles();
}
}
}
void SectorView()
{
if(SelectedFile?.ImageFormat is null) return;
var view = new HexViewWindow();
var vm = new HexViewWindowViewModel(_view, view, SelectedFile.ImageFormat, SelectedFile.Path);
view.DataContext = vm;
view.Show();
_view.Hide();
}
2025-10-15 21:25:05 +01:00
void Exit()
{
var lifetime = Application.Current!.ApplicationLifetime as IControlledApplicationLifetime;
lifetime!.Shutdown();
}
public void LoadComplete()
2025-10-15 22:54:35 +01:00
{
LoadFiles();
}
public void LoadFiles()
2025-10-15 21:25:05 +01:00
{
2025-10-16 01:23:17 +01:00
IsStatusVisible = true;
Status = "Loading...";
CurrentPath = Directory.GetCurrentDirectory();
2025-10-15 22:54:35 +01:00
Files.Clear();
2025-10-15 22:44:09 +01:00
2025-10-15 22:05:55 +01:00
var parentDirectory = new FileModel
{
Filename = "..",
2025-10-15 22:54:35 +01:00
Path = Path.GetRelativePath(CurrentPath, ".."),
2025-10-15 22:05:55 +01:00
ForegroundBrush =
new SolidColorBrush(Color.Parse(DirColorsParser.Instance.DirectoryColor ??
2025-10-15 22:54:35 +01:00
DirColorsParser.Instance.NormalColor)),
IsDirectory = true
2025-10-15 22:05:55 +01:00
};
Files.Add(parentDirectory);
2025-10-15 22:54:35 +01:00
foreach(FileModel model in Directory.GetDirectories(CurrentPath, "*", SearchOption.TopDirectoryOnly)
.Select(directory => new FileModel
{
Path = directory,
Filename = Path.GetFileName(directory),
ForegroundBrush =
new SolidColorBrush(Color.Parse(DirColorsParser.Instance
.DirectoryColor ??
DirColorsParser.Instance.NormalColor)),
IsDirectory = true
}))
2025-10-15 22:05:55 +01:00
Files.Add(model);
2025-10-15 21:25:05 +01:00
2025-10-15 22:54:35 +01:00
foreach(string file in Directory.GetFiles(CurrentPath, "*", SearchOption.TopDirectoryOnly))
2025-10-15 22:05:55 +01:00
{
var model = new FileModel
{
Path = file,
Filename = Path.GetFileName(file)
};
string extension = Path.GetExtension(file);
model.ForegroundBrush =
new SolidColorBrush(Color.Parse(DirColorsParser.Instance.ExtensionColors.TryGetValue(extension,
out string? hex)
? hex
: DirColorsParser.Instance.NormalColor));
Files.Add(model);
}
2025-10-16 00:13:02 +01:00
_ = Task.Run(Worker);
}
void Worker()
{
2025-10-16 01:23:17 +01:00
IsStatusVisible = true;
Status = "Loading file information...";
2025-10-16 01:15:13 +01:00
foreach(FileModel file in Files)
{
try
{
file.FileInfo = new FileInfo(file.Path);
IFilter inputFilter = PluginRegister.Singleton.GetFilter(file.Path);
if(inputFilter is null) continue;
IBaseImage imageFormat = ImageFormat.Detect(inputFilter);
if(imageFormat is null) continue;
2025-10-16 11:03:35 +01:00
ErrorNumber opened = imageFormat.Open(inputFilter);
if(opened != ErrorNumber.NoError) continue;
2025-10-16 01:15:13 +01:00
StringBuilder sb = new();
if(!string.IsNullOrWhiteSpace(imageFormat.Info.Version))
sb.AppendLine($"[bold][#875fff]Format:[/] [italic][#af8787]{imageFormat.Format}[/][/] version [#af8787]{imageFormat.Info.Version}[/][/]");
2025-10-16 01:15:13 +01:00
else
sb.AppendLine($"[bold][#875fff]Format:[/] [italic][#af8787]{imageFormat.Format}[/][/][/]");
2025-10-16 01:15:13 +01:00
switch(string.IsNullOrWhiteSpace(imageFormat.Info.Application))
{
case false when !string.IsNullOrWhiteSpace(imageFormat.Info.ApplicationVersion):
sb.AppendLine($"[#875fff]Was created with [italic][#af8787]{imageFormat.Info.Application}[/][/] version [italic][#af8787]{imageFormat.Info.ApplicationVersion}[/][/][/]");
2025-10-16 01:15:13 +01:00
break;
case false:
sb.AppendLine($"[#875fff]Was created with[/] [italic][#af8787]{imageFormat.Info.Application}[/][/]");
2025-10-16 01:15:13 +01:00
break;
}
sb.AppendLine($"[#875fff]Image without headers is [lime]{imageFormat.Info.ImageSize}[/] bytes long[/]");
2025-10-16 01:15:13 +01:00
sb.AppendLine($"[#875fff]Contains a media of [lime]{imageFormat.Info.Sectors}[/] sectors[/]");
sb.AppendLine($"[#875fff]Maximum sector size of [teal]{imageFormat.Info.SectorSize}[/] bytes[/]");
sb.AppendLine($"[#875fff]Would be [aqua]{ByteSize.FromBytes(imageFormat.Info.Sectors * imageFormat.Info.SectorSize).Humanize()}[/][/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.Creator))
sb.AppendLine($"[bold][#875fff]Created by:[/][/] [green]{imageFormat.Info.Creator}[/]");
2025-10-16 01:15:13 +01:00
if(imageFormat.Info.CreationTime != DateTime.MinValue)
sb.AppendLine($"[#875fff]Created on[/] [#afd700]{imageFormat.Info.CreationTime}[/]");
2025-10-16 01:15:13 +01:00
if(imageFormat.Info.LastModificationTime != DateTime.MinValue)
sb.AppendLine($"[#875fff]Last modified on[/] [#afd700]{imageFormat.Info.LastModificationTime}[/]");
2025-10-16 01:15:13 +01:00
2025-10-18 10:24:03 +01:00
sb.AppendLine($"[#875fff]Contains a media of type[/] [italic][fuchsia]{imageFormat.Info.MediaType.Humanize()}[/][/]");
sb.AppendLine($"[#875fff]XML type:[/] [italic][#af8787]{imageFormat.Info.MetadataMediaType}[/][/]");
2025-10-16 01:15:13 +01:00
sb.AppendLine(imageFormat.Info.HasPartitions
? "[green]Has partitions[/]"
: "[red]Doesn\'t have partitions[/]");
2025-10-16 01:15:13 +01:00
sb.AppendLine(imageFormat.Info.HasSessions
? "[green]Has sessions[/]"
: "[red]Doesn\'t have sessions[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.Comments))
sb.AppendLine($"[bold][#875fff]Comments:[/][/] {imageFormat.Info.Comments}");
2025-10-16 01:15:13 +01:00
if(imageFormat.Info.MediaSequence != 0 && imageFormat.Info.LastMediaSequence != 0)
2025-10-16 11:03:35 +01:00
{
sb.AppendLine($"[#875fff]Media is number [teal]{imageFormat.Info.MediaSequence}[/]" +
"\n" +
$" on a set of [teal]{imageFormat.Info.LastMediaSequence}[/] medias[/]");
2025-10-16 11:03:35 +01:00
}
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.MediaTitle))
sb.AppendLine($"[bold][#875fff]Media title:[/][/] [italic]{imageFormat.Info.MediaTitle}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.MediaManufacturer))
sb.AppendLine($"[bold][#875fff]Media manufacturer:[/][/] [italic]{imageFormat.Info.MediaManufacturer}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.MediaModel))
sb.AppendLine($"[bold][#875fff]Media model:[/][/] [italic]{imageFormat.Info.MediaModel}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.MediaSerialNumber))
sb.AppendLine($"[bold][#875fff]Media serial number:[/][/] [italic]{imageFormat.Info.MediaSerialNumber}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.MediaBarcode))
sb.AppendLine($"[bold][#875fff]Media barcode:[/][/] [italic]{imageFormat.Info.MediaBarcode}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.MediaPartNumber))
sb.AppendLine($"[bold][#875fff]Media part number:[/][/] [italic]{imageFormat.Info.MediaPartNumber}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.DriveManufacturer))
sb.AppendLine($"[bold][#875fff]Drive manufacturer:[/][/] [italic]{imageFormat.Info.DriveManufacturer}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.DriveModel))
sb.AppendLine($"[bold][#875fff]Drive model:[/][/] [italic]{imageFormat.Info.DriveModel}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.DriveSerialNumber))
sb.AppendLine($"[bold][#875fff]Drive serial number:[/][/] [italic]{imageFormat.Info.DriveSerialNumber}[/]");
2025-10-16 01:15:13 +01:00
if(!string.IsNullOrWhiteSpace(imageFormat.Info.DriveFirmwareRevision))
sb.AppendLine($"[bold][#875fff]Drive firmware info:[/][/] [italic]{imageFormat.Info.DriveFirmwareRevision}[/]");
2025-10-16 01:15:13 +01:00
if(imageFormat.Info.Cylinders > 0 &&
imageFormat.Info is { Heads: > 0, SectorsPerTrack: > 0 } &&
imageFormat.Info.MetadataMediaType != MetadataMediaType.OpticalDisc &&
imageFormat is not ITapeImage { IsTape: true })
sb.AppendLine($"[bold][#875fff]Media geometry:[/] [italic][teal]{imageFormat.Info.Cylinders}[/] cylinders, [teal]{imageFormat.Info.Heads}[/] heads, [teal]{imageFormat.Info.SectorsPerTrack}[/] sectors per track[/][/][/]");
2025-10-16 01:15:13 +01:00
if(imageFormat.Info.ReadableMediaTags is { Count: > 0 })
{
sb.AppendLine($"[bold][blue]Contains[/] [teal]{imageFormat.Info.ReadableMediaTags.Count}[/] [blue]readable media tags:[/][/]");
2025-10-16 01:15:13 +01:00
foreach(MediaTagType tag in imageFormat.Info.ReadableMediaTags.Order())
sb.Append($"[italic][#af8787]{tag}[/][/] ");
2025-10-16 01:15:13 +01:00
sb.AppendLine();
}
if(imageFormat.Info.ReadableSectorTags is { Count: > 0 })
{
sb.AppendLine($"[bold][blue]Contains [teal]{imageFormat.Info.ReadableSectorTags.Count}[/] [blue]readable sector tags:[/][/]");
2025-10-16 01:15:13 +01:00
foreach(SectorTagType tag in imageFormat.Info.ReadableSectorTags.Order())
sb.Append($"[italic][#af8787]{tag}[/][/] ");
2025-10-16 01:15:13 +01:00
sb.AppendLine();
}
file.Information = sb.ToString();
2025-10-16 11:03:35 +01:00
2025-10-16 11:43:03 +01:00
file.ImageFormat = imageFormat as IMediaImage;
2025-10-16 01:15:13 +01:00
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
}
}
2025-10-16 01:23:17 +01:00
Status = "Done.";
IsStatusVisible = false;
2025-10-15 21:25:05 +01:00
}
2025-10-15 22:54:35 +01:00
void OpenSelectedFile()
{
2025-10-16 11:03:35 +01:00
if(SelectedFile.IsDirectory)
{
CurrentPath = SelectedFile.Path;
Environment.CurrentDirectory = CurrentPath;
LoadFiles();
return;
2025-10-16 11:03:35 +01:00
}
2025-10-15 22:54:35 +01:00
2025-10-16 11:03:35 +01:00
if(SelectedFile.ImageFormat is null) return;
2025-10-16 11:57:23 +01:00
var imageWindow = new ImageWindow();
2025-10-16 12:06:45 +01:00
var imageViewModel = new ImageWindowViewModel(_view, imageWindow, SelectedFile.ImageFormat, SelectedFile.Path);
2025-10-16 11:57:23 +01:00
2025-10-16 11:03:35 +01:00
imageWindow.DataContext = imageViewModel;
imageWindow.Show();
2025-10-16 11:57:23 +01:00
_view.Hide();
2025-10-15 22:54:35 +01:00
}
bool CanOpenSelectedFile() => SelectedFile != null;
2025-10-15 21:25:05 +01:00
}