// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : MetadataEditorViewModel.cs // Author(s) : Natalia Portillo // // Component : GUI view models. // // --[ Description ] ---------------------------------------------------------- // // Metadata editor 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.Linq; using System.Text.Json; using System.Threading.Tasks; using Aaru.CommonTypes.AaruMetadata; using Aaru.Gui.Helpers; using Aaru.Gui.Localization; using Aaru.Localization; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using JetBrains.Annotations; using MsBox.Avalonia; using MsBox.Avalonia.Enums; using Sentry; using File = System.IO.File; namespace Aaru.Gui.ViewModels.Windows; public sealed partial class MetadataEditorViewModel : ViewModelBase { readonly Window _view; [ObservableProperty] ObservableCollection _advertisements = []; [ObservableProperty] ObservableCollection> _architectures = []; [ObservableProperty] ObservableCollection _audioMedias = []; [ObservableProperty] ObservableCollection _authors = []; // Complex object lists [ObservableProperty] ObservableCollection _barcodes = []; [ObservableProperty] ObservableCollection _blockMedias = []; [ObservableProperty] ObservableCollection _books = []; [ObservableProperty] ObservableCollection _categories = []; // String lists [ObservableProperty] ObservableCollection _developers = []; [ObservableProperty] string _filePath; [ObservableProperty] ObservableCollection _keywords = []; // Enum lists [ObservableProperty] ObservableCollection> _languages = []; [ObservableProperty] ObservableCollection _linearMedias = []; [ObservableProperty] ObservableCollection _magazines = []; // Basic metadata fields [ObservableProperty] string _name; [ObservableProperty] ObservableCollection _opticalDiscs = []; [ObservableProperty] string _partNumber; [ObservableProperty] ObservableCollection _pciCards = []; [ObservableProperty] ObservableCollection _performers = []; [ObservableProperty] ObservableCollection _publishers = []; [ObservableProperty] DateTime? _releaseDate; [ObservableProperty] ObservableCollection _requiredOperatingSystems = []; [ObservableProperty] LocalizedEnumValue _selectedReleaseType; [ObservableProperty] string _serialNumber; [ObservableProperty] ObservableCollection _subcategories = []; [ObservableProperty] ObservableCollection _systems = []; [ObservableProperty] string _title; [ObservableProperty] ObservableCollection _userManuals = []; [ObservableProperty] string _version; public MetadataEditorViewModel(Window view, [CanBeNull] string existingFilePath = null) { _view = view; _title = existingFilePath == null ? GUI.Title_Create_Metadata : GUI.Title_Edit_Metadata; _filePath = existingFilePath; if(!string.IsNullOrEmpty(existingFilePath) && File.Exists(existingFilePath)) LoadMetadata(existingFilePath); } // Available enum values for ComboBoxes [NotNull] public IEnumerable> AvailableReleaseTypes => LocalizedEnumHelper .GetLocalizedValues() .OrderBy(static x => x.Description); [NotNull] public IEnumerable> AvailableLanguages => LocalizedEnumHelper.GetLocalizedValues().OrderBy(static x => x.Description); [NotNull] public IEnumerable> AvailableArchitectures => LocalizedEnumHelper .GetLocalizedValues() .OrderBy(static x => x.Description); [NotNull] public IEnumerable> AvailableBarcodeTypes => LocalizedEnumHelper .GetLocalizedValues() .OrderBy(static x => x.Description); void LoadMetadata([NotNull] string path) { try { string json = File.ReadAllText(path); MetadataJson metadataJson = JsonSerializer.Deserialize(json, MetadataJsonContext.Default.MetadataJson); if(metadataJson?.AaruMetadata == null) return; Metadata metadata = metadataJson.AaruMetadata; // Basic fields Name = metadata.Name; Version = metadata.Version; SelectedReleaseType = metadata.Release.HasValue ? new LocalizedEnumValue(metadata.Release.Value) : null; ReleaseDate = metadata.ReleaseDate; PartNumber = metadata.PartNumber; SerialNumber = metadata.SerialNumber; // String lists LoadStringList(metadata.Developers, Developers); LoadStringList(metadata.Publishers, Publishers); LoadStringList(metadata.Authors, Authors); LoadStringList(metadata.Performers, Performers); LoadStringList(metadata.Keywords, Keywords); LoadStringList(metadata.Categories, Categories); LoadStringList(metadata.Subcategories, Subcategories); LoadStringList(metadata.Systems, Systems); // Enum lists if(metadata.Languages != null) foreach(Language lang in metadata.Languages) Languages.Add(new LocalizedEnumValue(lang)); if(metadata.Architectures != null) { foreach(Architecture arch in metadata.Architectures) Architectures.Add(new LocalizedEnumValue(arch)); } // Complex objects if(metadata.Barcodes != null) foreach(Barcode barcode in metadata.Barcodes) Barcodes.Add(new BarcodeViewModel(barcode)); if(metadata.Magazines != null) foreach(Magazine magazine in metadata.Magazines) Magazines.Add(new MagazineViewModel(magazine)); if(metadata.Books != null) foreach(Book book in metadata.Books) Books.Add(new BookViewModel(book)); if(metadata.RequiredOperatingSystems != null) { foreach(RequiredOperatingSystem os in metadata.RequiredOperatingSystems) RequiredOperatingSystems.Add(new RequiredOperatingSystemViewModel(os)); } if(metadata.UserManuals != null) foreach(UserManual manual in metadata.UserManuals) UserManuals.Add(new UserManualViewModel(manual)); if(metadata.OpticalDiscs != null) foreach(OpticalDisc disc in metadata.OpticalDiscs) OpticalDiscs.Add(new OpticalDiscViewModel(disc)); if(metadata.Advertisements != null) foreach(Advertisement ad in metadata.Advertisements) Advertisements.Add(new AdvertisementViewModel(ad)); if(metadata.LinearMedias != null) foreach(LinearMedia media in metadata.LinearMedias) LinearMedias.Add(new LinearMediaViewModel(media)); if(metadata.PciCards != null) foreach(Pci pci in metadata.PciCards) PciCards.Add(new PciViewModel(pci)); if(metadata.BlockMedias != null) foreach(BlockMedia media in metadata.BlockMedias) BlockMedias.Add(new BlockMediaViewModel(media)); if(metadata.AudioMedias != null) foreach(AudioMedia media in metadata.AudioMedias) AudioMedias.Add(new AudioMediaViewModel(media)); } catch(Exception ex) { _ = MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, string.Format(GUI.Error_Loading_metadata, ex.Message), ButtonEnum.Ok, Icon.Error) .ShowAsync(); } } static void LoadStringList([CanBeNull] List source, ObservableCollection target) { if(source == null) return; target.Clear(); foreach(string item in source) target.Add(item); } static void LoadEnumList([CanBeNull] List source, ObservableCollection target) where T : struct, Enum { if(source == null) return; target.Clear(); foreach(T item in source) target.Add(item); } [RelayCommand] async Task SaveAsync() { try { var metadata = new Metadata { Name = Name, Version = Version, Release = SelectedReleaseType?.Value, ReleaseDate = ReleaseDate, PartNumber = PartNumber, SerialNumber = SerialNumber, Developers = Developers.Any() ? [..Developers] : null, Publishers = Publishers.Any() ? [..Publishers] : null, Authors = Authors.Any() ? [..Authors] : null, Performers = Performers.Any() ? [..Performers] : null, Keywords = Keywords.Any() ? [..Keywords] : null, Categories = Categories.Any() ? [..Categories] : null, Subcategories = Subcategories.Any() ? [..Subcategories] : null, Systems = Systems.Any() ? [..Systems] : null, Languages = Languages.Any() ? [..Languages.Select(l => l.Value)] : null, Architectures = Architectures.Any() ? [..Architectures.Select(a => a.Value)] : null, Barcodes = Barcodes.Any() ? [..Barcodes.Select(static b => b.ToModel())] : null, Magazines = Magazines.Any() ? [..Magazines.Select(static m => m.ToModel())] : null, Books = Books.Any() ? [..Books.Select(static b => b.ToModel())] : null, RequiredOperatingSystems = RequiredOperatingSystems.Any() ? [..RequiredOperatingSystems.Select(static os => os.ToModel())] : null, UserManuals = UserManuals.Any() ? [..UserManuals.Select(static um => um.ToModel())] : null, OpticalDiscs = OpticalDiscs.Any() ? [..OpticalDiscs.Select(static od => od.ToModel())] : null, Advertisements = Advertisements.Any() ? [..Advertisements.Select(static a => a.ToModel())] : null, LinearMedias = LinearMedias.Any() ? [..LinearMedias.Select(static lm => lm.ToModel())] : null, PciCards = PciCards.Any() ? [..PciCards.Select(static p => p.ToModel())] : null, BlockMedias = BlockMedias.Any() ? [..BlockMedias.Select(static bm => bm.ToModel())] : null, AudioMedias = AudioMedias.Any() ? [..AudioMedias.Select(static am => am.ToModel())] : null }; var metadataJson = new MetadataJson { AaruMetadata = metadata }; string savePath = FilePath; if(string.IsNullOrEmpty(savePath)) { var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; Window mainWindow = lifetime?.MainWindow; if(mainWindow == null) return; IStorageFile file = await mainWindow.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = GUI.Dialog_Save_Metadata_File, FileTypeChoices = [ new FilePickerFileType(GUI.FileType_JSON) { Patterns = ["*.json"] } ], DefaultExtension = "json" }); if(file == null) return; savePath = file.Path.LocalPath; FilePath = savePath; } string json = JsonSerializer.Serialize(metadataJson, MetadataJsonContext.Default.MetadataJson); await File.WriteAllTextAsync(savePath, json); await MessageBoxManager.GetMessageBoxStandard(GUI.Title_Success, GUI.Message_Metadata_saved_successfully, ButtonEnum.Ok, Icon.Success) .ShowAsync(); _view.Close(); } catch(Exception ex) { SentrySdk.CaptureException(ex); await MessageBoxManager.GetMessageBoxStandard(UI.Title_Error, string.Format(GUI.Error_Saving_metadata, ex.Message), ButtonEnum.Ok, Icon.Error) .ShowAsync(); } } [RelayCommand] void Cancel() { _view.Close(); } // Commands for adding items to simple string lists [RelayCommand] void AddDeveloper() => Developers.Add(string.Empty); [RelayCommand] void RemoveDeveloper(string item) => Developers.Remove(item); [RelayCommand] void AddPublisher() => Publishers.Add(string.Empty); [RelayCommand] void RemovePublisher(string item) => Publishers.Remove(item); [RelayCommand] void AddAuthor() => Authors.Add(string.Empty); [RelayCommand] void RemoveAuthor(string item) => Authors.Remove(item); [RelayCommand] void AddPerformer() => Performers.Add(string.Empty); [RelayCommand] void RemovePerformer(string item) => Performers.Remove(item); [RelayCommand] void AddKeyword() => Keywords.Add(string.Empty); [RelayCommand] void RemoveKeyword(string item) => Keywords.Remove(item); [RelayCommand] void AddCategory() => Categories.Add(string.Empty); [RelayCommand] void RemoveCategory(string item) => Categories.Remove(item); [RelayCommand] void AddSubcategory() => Subcategories.Add(string.Empty); [RelayCommand] void RemoveSubcategory(string item) => Subcategories.Remove(item); [RelayCommand] void AddSystem() => Systems.Add(string.Empty); [RelayCommand] void RemoveSystem(string item) => Systems.Remove(item); // Commands for adding items to enum lists [RelayCommand] void AddLanguage(object parameter) { if(parameter is LocalizedEnumValue langValue) if(!Languages.Any(l => l.Value == langValue.Value)) Languages.Add(langValue); } [RelayCommand] void RemoveLanguage(LocalizedEnumValue language) => Languages.Remove(language); [RelayCommand] void AddArchitecture(object parameter) { if(parameter is LocalizedEnumValue archValue) if(!Architectures.Any(a => a.Value == archValue.Value)) Architectures.Add(archValue); } [RelayCommand] void RemoveArchitecture(LocalizedEnumValue architecture) => Architectures.Remove(architecture); // Commands for complex objects [RelayCommand] void AddBarcode() => Barcodes.Add(new BarcodeViewModel()); [RelayCommand] void RemoveBarcode(BarcodeViewModel item) => Barcodes.Remove(item); [RelayCommand] void AddMagazine() => Magazines.Add(new MagazineViewModel()); [RelayCommand] void RemoveMagazine(MagazineViewModel item) => Magazines.Remove(item); [RelayCommand] void AddBook() => Books.Add(new BookViewModel()); [RelayCommand] void RemoveBook(BookViewModel item) => Books.Remove(item); [RelayCommand] void AddRequiredOperatingSystem() => RequiredOperatingSystems.Add(new RequiredOperatingSystemViewModel()); [RelayCommand] void RemoveRequiredOperatingSystem(RequiredOperatingSystemViewModel item) => RequiredOperatingSystems.Remove(item); [RelayCommand] void AddUserManual() => UserManuals.Add(new UserManualViewModel()); [RelayCommand] void RemoveUserManual(UserManualViewModel item) => UserManuals.Remove(item); [RelayCommand] void AddOpticalDisc() => OpticalDiscs.Add(new OpticalDiscViewModel()); [RelayCommand] void RemoveOpticalDisc(OpticalDiscViewModel item) => OpticalDiscs.Remove(item); [RelayCommand] void AddAdvertisement() => Advertisements.Add(new AdvertisementViewModel()); [RelayCommand] void RemoveAdvertisement(AdvertisementViewModel item) => Advertisements.Remove(item); [RelayCommand] void AddLinearMedia() => LinearMedias.Add(new LinearMediaViewModel()); [RelayCommand] void RemoveLinearMedia(LinearMediaViewModel item) => LinearMedias.Remove(item); [RelayCommand] void AddPciCard() => PciCards.Add(new PciViewModel()); [RelayCommand] void RemovePciCard(PciViewModel item) => PciCards.Remove(item); [RelayCommand] void AddBlockMedia() => BlockMedias.Add(new BlockMediaViewModel()); [RelayCommand] void RemoveBlockMedia(BlockMediaViewModel item) => BlockMedias.Remove(item); [RelayCommand] void AddAudioMedia() => AudioMedias.Add(new AudioMediaViewModel()); [RelayCommand] void RemoveAudioMedia(AudioMediaViewModel item) => AudioMedias.Remove(item); } // Helper ViewModels for complex objects public sealed partial class BarcodeViewModel : ObservableObject { [ObservableProperty] BarcodeType _type; [ObservableProperty] string _value; public BarcodeViewModel() {} public BarcodeViewModel([NotNull] Barcode barcode) { Type = barcode.Type; Value = barcode.Value; } [NotNull] public Barcode ToModel() => new() { Type = Type, Value = Value }; } public sealed partial class MagazineViewModel : ObservableObject { readonly Magazine _originalModel; [ObservableProperty] string _editorial; [ObservableProperty] string _name; [ObservableProperty] uint? _number; [ObservableProperty] uint? _pages; [ObservableProperty] string _pageSize; [ObservableProperty] DateTime? _publicationDate; public MagazineViewModel() => _originalModel = new Magazine(); public MagazineViewModel([NotNull] Magazine magazine) { _originalModel = magazine; Name = magazine.Name; Editorial = magazine.Editorial; PublicationDate = magazine.PublicationDate; Number = magazine.Number; Pages = magazine.Pages; PageSize = magazine.PageSize; } [NotNull] public Magazine ToModel() { // Update only the editable fields, preserve all others (Barcodes, Cover, Languages, Scan) _originalModel.Name = Name; _originalModel.Editorial = Editorial; _originalModel.PublicationDate = PublicationDate; _originalModel.Number = Number; _originalModel.Pages = Pages; _originalModel.PageSize = PageSize; return _originalModel; } } public sealed partial class BookViewModel : ObservableObject { readonly Book _originalModel; [ObservableProperty] string _author; [ObservableProperty] string _editorial; [ObservableProperty] string _name; [ObservableProperty] uint? _pages; [ObservableProperty] string _pageSize; [ObservableProperty] DateTime? _publicationDate; public BookViewModel() => _originalModel = new Book(); public BookViewModel([NotNull] Book book) { _originalModel = book; Name = book.Name; Editorial = book.Editorial; Author = book.Author; PublicationDate = book.PublicationDate; Pages = book.Pages; PageSize = book.PageSize; } [NotNull] public Book ToModel() { // Update only the editable fields, preserve all others (Barcodes, Cover, Languages, Scan) _originalModel.Name = Name; _originalModel.Editorial = Editorial; _originalModel.Author = Author; _originalModel.PublicationDate = PublicationDate; _originalModel.Pages = Pages; _originalModel.PageSize = PageSize; return _originalModel; } } public sealed partial class RequiredOperatingSystemViewModel : ObservableObject { [ObservableProperty] string _name; [ObservableProperty] ObservableCollection _versions = []; public RequiredOperatingSystemViewModel() {} public RequiredOperatingSystemViewModel([NotNull] RequiredOperatingSystem os) { Name = os.Name; if(os.Versions == null) return; foreach(string version in os.Versions) Versions.Add(version); } [NotNull] public RequiredOperatingSystem ToModel() => new() { Name = Name, Versions = Versions.Any() ? [..Versions] : null }; } public sealed partial class UserManualViewModel : ObservableObject { readonly UserManual _originalModel; [ObservableProperty] uint _pages; [ObservableProperty] string _pageSize; public UserManualViewModel() => _originalModel = new UserManual(); public UserManualViewModel([NotNull] UserManual manual) { _originalModel = manual; Pages = manual.Pages; PageSize = manual.PageSize; } [NotNull] public UserManual ToModel() { // Update only the editable fields, preserve all others (Language, Scan) _originalModel.Pages = Pages; _originalModel.PageSize = PageSize; return _originalModel; } } // Simplified ViewModels for complex media types (can be expanded as needed) public sealed partial class OpticalDiscViewModel : ObservableObject { readonly OpticalDisc _originalModel; [ObservableProperty] string _discSubType; [ObservableProperty] string _discType; [ObservableProperty] string _partNumber; [ObservableProperty] string _serialNumber; public OpticalDiscViewModel() => _originalModel = new OpticalDisc(); public OpticalDiscViewModel([NotNull] OpticalDisc disc) { _originalModel = disc; PartNumber = disc.PartNumber; SerialNumber = disc.SerialNumber; DiscType = disc.DiscType; DiscSubType = disc.DiscSubType; } [NotNull] public OpticalDisc ToModel() { // Update only the editable fields, preserve all others _originalModel.PartNumber = PartNumber; _originalModel.SerialNumber = SerialNumber; _originalModel.DiscType = DiscType; _originalModel.DiscSubType = DiscSubType; return _originalModel; } } public sealed partial class AdvertisementViewModel : ObservableObject { readonly Advertisement _originalModel; [ObservableProperty] string _manufacturer; [ObservableProperty] string _product; public AdvertisementViewModel() => _originalModel = new Advertisement(); public AdvertisementViewModel([NotNull] Advertisement ad) { _originalModel = ad; Manufacturer = ad.Manufacturer; Product = ad.Product; } [NotNull] public Advertisement ToModel() { // Update only the editable fields, preserve all others (File, FileSize, Frames, Duration, etc.) _originalModel.Manufacturer = Manufacturer; _originalModel.Product = Product; return _originalModel; } } public sealed partial class LinearMediaViewModel : ObservableObject { readonly LinearMedia _originalModel; [ObservableProperty] string _manufacturer; [ObservableProperty] string _model; [ObservableProperty] string _partNumber; [ObservableProperty] string _serialNumber; public LinearMediaViewModel() => _originalModel = new LinearMedia(); public LinearMediaViewModel([NotNull] LinearMedia media) { _originalModel = media; PartNumber = media.PartNumber; SerialNumber = media.SerialNumber; Manufacturer = media.Manufacturer; Model = media.Model; } [NotNull] public LinearMedia ToModel() { // Update only the editable fields, preserve all others _originalModel.PartNumber = PartNumber; _originalModel.SerialNumber = SerialNumber; _originalModel.Manufacturer = Manufacturer; _originalModel.Model = Model; return _originalModel; } } public sealed partial class PciViewModel : ObservableObject { readonly Pci _originalModel; [ObservableProperty] ushort _deviceID; [ObservableProperty] ushort _vendorID; public PciViewModel() => _originalModel = new Pci(); public PciViewModel([NotNull] Pci pci) { _originalModel = pci; VendorID = pci.VendorID; DeviceID = pci.DeviceID; } [NotNull] public Pci ToModel() { // Update only the editable fields, preserve all others _originalModel.VendorID = VendorID; _originalModel.DeviceID = DeviceID; return _originalModel; } } public sealed partial class BlockMediaViewModel : ObservableObject { readonly BlockMedia _originalModel; [ObservableProperty] string _firmware; [ObservableProperty] string _manufacturer; [ObservableProperty] string _model; [ObservableProperty] string _serial; public BlockMediaViewModel() => _originalModel = new BlockMedia(); public BlockMediaViewModel([NotNull] BlockMedia media) { _originalModel = media; Manufacturer = media.Manufacturer; Model = media.Model; Serial = media.Serial; Firmware = media.Firmware; } [NotNull] public BlockMedia ToModel() { // Update only the editable fields, preserve all others _originalModel.Manufacturer = Manufacturer; _originalModel.Model = Model; _originalModel.Serial = Serial; _originalModel.Firmware = Firmware; return _originalModel; } } public sealed partial class AudioMediaViewModel : ObservableObject { readonly AudioMedia _originalModel; [ObservableProperty] string _manufacturer; [ObservableProperty] string _model; [ObservableProperty] string _partNumber; [ObservableProperty] string _serialNumber; public AudioMediaViewModel() => _originalModel = new AudioMedia(); public AudioMediaViewModel([NotNull] AudioMedia media) { _originalModel = media; Manufacturer = media.Manufacturer; Model = media.Model; PartNumber = media.PartNumber; SerialNumber = media.SerialNumber; } [NotNull] public AudioMedia ToModel() { // Update only the editable fields, preserve all others _originalModel.Manufacturer = Manufacturer; _originalModel.Model = Model; _originalModel.PartNumber = PartNumber; _originalModel.SerialNumber = SerialNumber; return _originalModel; } }