diff --git a/UI/Assets/Resources.resx b/UI/Assets/Resources.resx index e997b12..ffda73b 100644 --- a/UI/Assets/Resources.resx +++ b/UI/Assets/Resources.resx @@ -56,9 +56,6 @@ Total tags: {0} - - Total tags: 0 - _Help @@ -146,9 +143,6 @@ You have unsaved changes to the currently loaded tag. Do you want to save them now? - - Unsaved changes - No tags found. @@ -188,4 +182,46 @@ Tags: {0}, with {1} parent-child relationships + + Import + + + File to import: + + + Importing... + + + Import failed. + + + Database file to save to: + + + Browse... + + + No file picked. + + + Import... + + + Error + + + Export tag database to file... + + + Save tag database file... + + + Open tag database... + + + Choose template to import... + + + File type '{0}' is not supported. + \ No newline at end of file diff --git a/UI/ViewModels/ErrorDialogViewModel.cs b/UI/ViewModels/ErrorDialogViewModel.cs index b87f48d..cd8d4f8 100644 --- a/UI/ViewModels/ErrorDialogViewModel.cs +++ b/UI/ViewModels/ErrorDialogViewModel.cs @@ -1,4 +1,7 @@ -using Avalonia.Controls; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using CommunityToolkit.Mvvm.ComponentModel; using TagHierarchyManager.UI.Assets; using TagHierarchyManager.UI.Views; @@ -13,14 +16,18 @@ public partial class ErrorDialogViewModel : ViewModelBase { this.ErrorMessage = message; } - - public void ShowDialog(Window window) + + public void ShowDialog() { + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) + return; + ErrorDialog error = new() { DataContext = this, Title = Resources.ErrorDialogTitle }; - error.ShowDialog(window); + var ownerWindow = desktop.Windows.FirstOrDefault(w => w.IsActive) ?? desktop.MainWindow; + error.ShowDialog(ownerWindow); } } \ No newline at end of file diff --git a/UI/ViewModels/ImportDialogViewModel.cs b/UI/ViewModels/ImportDialogViewModel.cs new file mode 100644 index 0000000..0c366c2 --- /dev/null +++ b/UI/ViewModels/ImportDialogViewModel.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using TagHierarchyManager.UI.Assets; + +namespace TagHierarchyManager.UI.ViewModels; + +public partial class ImportDialogViewModel : ViewModelBase +{ + public event Action? RequestClose; + + public ImportDialogViewModel(MainWindowViewModel mainWindow) + { + this.MainWindow = mainWindow; + } + + public string VisibleTemplateFilePath => + !string.IsNullOrWhiteSpace(TemplateFilePath) ? this.TemplateFilePath : Resources.ImportNoFilePicked; + + public string VisibleDatabaseFilePath => + !string.IsNullOrWhiteSpace(DatabaseFilePath) ? this.DatabaseFilePath : Resources.ImportNoFilePicked; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(VisibleTemplateFilePath))] + [NotifyPropertyChangedFor(nameof(BothFilesSelected))] + private string _templateFilePath; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(VisibleDatabaseFilePath))] + [NotifyPropertyChangedFor(nameof(BothFilesSelected))] + private string _databaseFilePath; + + [ObservableProperty] private string _importStatus = string.Empty; + + public bool BothFilesSelected => !string.IsNullOrWhiteSpace(TemplateFilePath) && !string.IsNullOrWhiteSpace(DatabaseFilePath); + + private MainWindowViewModel MainWindow { get; } + + public async Task InitiateImport() + { + try + { + ImportStatus = Assets.Resources.ImportStatusInProgress; + await this.MainWindow.CreateNewDatabase(DatabaseFilePath, TemplateFilePath); + RequestClose?.Invoke(); + } + catch (Exception ex) + { + ImportStatus = Assets.Resources.ImportStatusFailed; + var error = new ErrorDialogViewModel(ex.Message); + error.ShowDialog(); + } + } +} \ No newline at end of file diff --git a/UI/ViewModels/MainWindowViewModel.cs b/UI/ViewModels/MainWindowViewModel.cs index 50b3a83..0e28075 100644 --- a/UI/ViewModels/MainWindowViewModel.cs +++ b/UI/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; @@ -10,6 +11,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using TagHierarchyManager.Common; using TagHierarchyManager.Exporters; +using TagHierarchyManager.Importers; using TagHierarchyManager.Models; using TagHierarchyManager.UI.Assets; using TagHierarchyManager.UI.Views; @@ -50,7 +52,7 @@ public partial class MainWindowViewModel : ViewModelBase set { if (this._selectedTag == value || this._isSwitching) return; - if (this._isDbEnabled == false && value == null) this._selectedTag = value; + if (!this.IsDbEnabled && value == null) this._selectedTag = value; if (this._selectedTag != null && this.UnsavedChanges) { _ = this.HandleTagSwitchAsync(this._selectedTag, value); @@ -66,25 +68,47 @@ public partial class MainWindowViewModel : ViewModelBase } } - public async Task CreateNewDatabase(string filePath) + public async Task CreateNewDatabase(string filePath, string? templateFilePath = null) { if (this.Database != null) this.UninitialiseDatabase(); - + + Dictionary? tagsToImport = null; + TagDatabase db = new(); + db.InitialisationComplete += this.TagDatabase_OnInitalisationComplete; + try { - TagDatabase db = new(); - db.InitialisationComplete += this.TagDatabase_OnInitalisationComplete; + if (string.IsNullOrWhiteSpace(filePath)) return; + + if (templateFilePath is not null && File.Exists(templateFilePath)) + { + Importer importer = PickImporterFromFileExt(templateFilePath); + tagsToImport = await importer.ImportFromFileAsync(templateFilePath); + } + // overwrite is set to true here for now since the OS should handle the overwrite request. // will need to remove once Terminal.Gui is fully replaced. - await Task.Run(() => db.CreateAsync(filePath, true)); + await Task.Run(() => db.CreateAsync(filePath, true, tagsToImport)); } catch (Exception ex) { - this.UninitialiseDatabase(); - this.ShowErrorDialog(ex.Message); + db.InitialisationComplete -= this.TagDatabase_OnInitalisationComplete; + if (templateFilePath is null) this.ShowErrorDialog(ex.Message); + else throw; } + } + + private Importer PickImporterFromFileExt(string path) + { + string fileExt = Path.GetExtension(path); + + // if more importers are added, convert this to a switch statement based on file extension. + // ReSharper disable once ConvertIfStatementToReturnStatement + if (fileExt == FileTypes.MusicBeeTagHierarchyTemplate.FileExtension) + return new MusicBeeTagHierarchyImporter(); + throw new NotSupportedException(string.Format(Resources.ErrorImportFileTypeNotSupported, fileExt)); } public async Task ExportAsync(string path) @@ -122,7 +146,6 @@ public partial class MainWindowViewModel : ViewModelBase } catch (Exception ex) { - this.UninitialiseDatabase(); this.ShowErrorDialog(ex.Message); } @@ -173,13 +196,22 @@ public partial class MainWindowViewModel : ViewModelBase } public void ShowErrorDialog(string message) + { + var error = new ErrorDialogViewModel(message); + error.ShowDialog(); + } + + public void ShowImportDialog() { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return; + + var dialog = new ImportDialog + { + DataContext = new ImportDialogViewModel(this) + }; - var mainWindow = desktop.MainWindow; - var error = new ErrorDialogViewModel(message); - error.ShowDialog(mainWindow); + dialog.ShowDialog(desktop.MainWindow!); } public async Task ShowNullableBoolDialog(Window dialog) diff --git a/UI/Views/Common.cs b/UI/Views/Common.cs new file mode 100644 index 0000000..5ff407c --- /dev/null +++ b/UI/Views/Common.cs @@ -0,0 +1,16 @@ +using Avalonia.Platform.Storage; + +namespace TagHierarchyManager.UI.Views; + +public class Common +{ + public static FilePickerFileType MusicBeeTagHierarchy { get; } = new(Assets.Resources.FileFormatMusicBeeTagHierarchy) + { + Patterns = ["*.txt"] + }; + + public static FilePickerFileType TagDatabaseFileType { get; } = new(Assets.Resources.FileFormatTagHierarchyDatabase) + { + Patterns = ["*.thdb"] + }; +} \ No newline at end of file diff --git a/UI/Views/ErrorDialog.axaml b/UI/Views/ErrorDialog.axaml index 227985b..a89e9ec 100644 --- a/UI/Views/ErrorDialog.axaml +++ b/UI/Views/ErrorDialog.axaml @@ -4,14 +4,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:assets="clr-namespace:TagHierarchyManager.UI.Assets" xmlns:vm="clr-namespace:TagHierarchyManager.UI.ViewModels" - mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="160" - Width="500" Height="160" CanResize="False" + mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="125" + MinWidth="500" MinHeight="125" CanResize="False" SizeToContent="WidthAndHeight" x:Class="TagHierarchyManager.UI.Views.ErrorDialog" x:DataType="vm:ErrorDialogViewModel" - Title="ErrorDialog"> + Title="{x:Static assets:Resources.ErrorDialogTitleBarTitle}"> - + +