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}">
-
+
diff --git a/UI/Views/ImportDialog.axaml b/UI/Views/ImportDialog.axaml
new file mode 100644
index 0000000..c0f0077
--- /dev/null
+++ b/UI/Views/ImportDialog.axaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/Views/ImportDialog.axaml.cs b/UI/Views/ImportDialog.axaml.cs
new file mode 100644
index 0000000..475837b
--- /dev/null
+++ b/UI/Views/ImportDialog.axaml.cs
@@ -0,0 +1,77 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
+using TagHierarchyManager.UI.ViewModels;
+
+namespace TagHierarchyManager.UI.Views;
+
+public partial class ImportDialog : Window
+{
+ public ImportDialog()
+ {
+ InitializeComponent();
+ this.DataContextChanged += (s, e) =>
+ {
+ if (this.ViewModel != null)
+ {
+ this.ViewModel.RequestClose += this.Close;
+ }
+ };
+ }
+
+ private ImportDialogViewModel? ViewModel => this.DataContext as ImportDialogViewModel;
+
+ public void ButtonCancel_Click(object? sender, RoutedEventArgs e) => this.Close();
+
+ public async void ButtonBrowseTemplate_Click(object? sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var files = await this.StorageProvider.OpenFilePickerAsync(
+ new FilePickerOpenOptions
+ {
+ AllowMultiple = false,
+ Title = Assets.Resources.DialogTitleChooseImportTemplate,
+ FileTypeFilter = [Common.MusicBeeTagHierarchy]
+ });
+ if (files.Count == 0) return;
+ var path = files[0].TryGetLocalPath();
+ if (path == null) return;
+ this.ViewModel?.TemplateFilePath = path;
+ }
+ catch (Exception ex)
+ {
+ this.ViewModel?.TemplateFilePath = string.Empty;
+ var error = new ErrorDialogViewModel(ex.Message);
+ error.ShowDialog();
+ }
+ }
+
+ public async void ButtonBrowseDatabase_Click(object? sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var file = await this.StorageProvider.SaveFilePickerAsync(
+ new FilePickerSaveOptions
+ {
+ Title = Assets.Resources.DialogTitleSaveDatabaseAs,
+ FileTypeChoices = [Common.TagDatabaseFileType]
+ });
+ var path = file?.TryGetLocalPath();
+ if (path == null) return;
+ this.ViewModel?.DatabaseFilePath = path;
+ }
+ catch (Exception ex)
+ {
+ this.ViewModel?.TemplateFilePath = string.Empty;
+ var error = new ErrorDialogViewModel(ex.Message);
+ error.ShowDialog();
+ }
+ }
+
+ public async void ButtonImport_Click(object? sender, RoutedEventArgs e)
+ {
+ await this.ViewModel.InitiateImport();
+ }
+}
\ No newline at end of file
diff --git a/UI/Views/MainWindow.axaml b/UI/Views/MainWindow.axaml
index 97cf7a1..5abe914 100644
--- a/UI/Views/MainWindow.axaml
+++ b/UI/Views/MainWindow.axaml
@@ -9,15 +9,11 @@
x:DataType="vm:MainWindowViewModel"
Closing="WindowClosing"
Title="{Binding WindowTitle}">
-
@@ -59,7 +55,7 @@
+ FontStyle="Italic" Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}">
@@ -72,11 +68,16 @@
Click="ButtonAdd_Click" />
-
+
-
+
diff --git a/UI/Views/MainWindow.axaml.cs b/UI/Views/MainWindow.axaml.cs
index 55fb675..0ac9824 100644
--- a/UI/Views/MainWindow.axaml.cs
+++ b/UI/Views/MainWindow.axaml.cs
@@ -18,16 +18,6 @@ public partial class MainWindow : Window
private MainWindowViewModel? ViewModel => this.DataContext as MainWindowViewModel;
- private static FilePickerFileType MusicBeeTagHierarchy { get; } = new(Assets.Resources.FileFormatMusicBeeTagHierarchy)
- {
- Patterns = ["*.txt"]
- };
-
- private static FilePickerFileType TagDatabaseFileType { get; } = new(Assets.Resources.FileFormatTagHierarchyDatabase)
- {
- Patterns = ["*.thdb"]
- };
-
public void ButtonAdd_Click(object? sender, RoutedEventArgs e)
{
this.ViewModel.NewTag();
@@ -66,23 +56,28 @@ public partial class MainWindow : Window
var file = await this.StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions
{
- Title = "Export tag database...",
- FileTypeChoices = [MusicBeeTagHierarchy],
+ Title = Assets.Resources.DialogTitleExportTagDatabase,
+ FileTypeChoices = [Common.MusicBeeTagHierarchy],
SuggestedFileName = this.ViewModel.Database?.Name
});
if (file == null) return;
var path = file.TryGetLocalPath();
await this.ViewModel.ExportAsync(path);
}
+
+ public void MenuItemImport_Click(object? sender, RoutedEventArgs e)
+ {
+ this.ViewModel.ShowImportDialog();
+ }
public async void MenuItemNew_Click(object? sender, RoutedEventArgs e)
{
- if (this.ViewModel.Database is null) return;
+ if (this.ViewModel is null) return;
var file = await this.StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions
{
- Title = "Create a new tag database...",
- FileTypeChoices = [MusicBeeTagHierarchy]
+ Title = Assets.Resources.DialogTitleSaveDatabaseAs,
+ FileTypeChoices = [Common.TagDatabaseFileType]
});
if (file == null) return;
var path = file.TryGetLocalPath();
@@ -95,14 +90,15 @@ public partial class MainWindow : Window
new FilePickerOpenOptions
{
AllowMultiple = false,
- Title = "Open a tag database...",
- FileTypeFilter = [TagDatabaseFileType]
+ Title = Assets.Resources.DialogTitleOpenDatabase,
+ FileTypeFilter = [Common.TagDatabaseFileType]
});
if (files.Count == 0) return;
var path = files[0].TryGetLocalPath();
if (path == null) return;
await this.ViewModel.LoadDatabase(path);
}
+
public void MenuItemQuit_Click(object? sender, RoutedEventArgs e)
{