diff --git a/Core/Importers/MusicBeeTagHierarchyImporter/MusicBeeTagHierarchyImporter.cs b/Core/Importers/MusicBeeTagHierarchyImporter/MusicBeeTagHierarchyImporter.cs
index 7e6b5e8..a4cc7ce 100644
--- a/Core/Importers/MusicBeeTagHierarchyImporter/MusicBeeTagHierarchyImporter.cs
+++ b/Core/Importers/MusicBeeTagHierarchyImporter/MusicBeeTagHierarchyImporter.cs
@@ -11,6 +11,8 @@ public partial class MusicBeeTagHierarchyImporter : Importer
private const int IndentSize = 4; // MusicBee is strict about having an indent size of 4 spaces.
private const string TagBindingSeparator = "::";
+ private static readonly string[] CommentSymbols = [";", "//"];
+
///
/// Initializes a new instance of the class.
///
@@ -63,7 +65,10 @@ public partial class MusicBeeTagHierarchyImporter : Importer
private static bool IsEmptyOrComment(string line)
{
- return string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line) || line.StartsWith(CommentSymbol);
+ if (CommentSymbols.Any(symbol => line.StartsWith(symbol, StringComparison.Ordinal)))
+ return true;
+
+ return string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line);
}
private static void UpdateParentStack(List parentStack, TagHierarchyLine currentLine, int previousIndent,
diff --git a/UI/App.axaml.cs b/UI/App.axaml.cs
index 191b2e0..41733d7 100644
--- a/UI/App.axaml.cs
+++ b/UI/App.axaml.cs
@@ -1,16 +1,17 @@
+using System.Linq;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
-using System.Linq;
using Avalonia.Markup.Xaml;
using TagHierarchyManager.UI.ViewModels;
using TagHierarchyManager.UI.Views;
namespace TagHierarchyManager.UI;
-public partial class App : Application
+public class App : Application
{
+ private TagDatabaseService? TagDatabaseService { get; set; }
+
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -18,14 +19,17 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ if (this.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
- DisableAvaloniaDataAnnotationValidation();
+ this.DisableAvaloniaDataAnnotationValidation();
+
+ this.TagDatabaseService = new TagDatabaseService();
+
desktop.MainWindow = new MainWindow
{
- DataContext = new MainWindowViewModel(),
+ DataContext = new MainWindowViewModel(this.TagDatabaseService)
};
}
@@ -39,9 +43,6 @@ public partial class App : Application
BindingPlugins.DataValidators.OfType().ToArray();
// remove each entry found
- foreach (var plugin in dataValidationPluginsToRemove)
- {
- BindingPlugins.DataValidators.Remove(plugin);
- }
+ foreach (var plugin in dataValidationPluginsToRemove) BindingPlugins.DataValidators.Remove(plugin);
}
}
\ No newline at end of file
diff --git a/UI/Assets/Resources.resx b/UI/Assets/Resources.resx
index 1cb8ef6..9f7be3d 100644
--- a/UI/Assets/Resources.resx
+++ b/UI/Assets/Resources.resx
@@ -256,10 +256,10 @@
Successfully added {0} tags.
-
-
-
Add multiple tags...
+
+ You have unsaved changes to the currently loaded tag. Are you sure you want to cancel?
+
\ No newline at end of file
diff --git a/UI/TagDatabaseService.cs b/UI/TagDatabaseService.cs
new file mode 100644
index 0000000..98ab242
--- /dev/null
+++ b/UI/TagDatabaseService.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using TagHierarchyManager.Common;
+using TagHierarchyManager.Exporters;
+using TagHierarchyManager.Importers;
+using TagHierarchyManager.Models;
+using TagHierarchyManager.UI.Assets;
+
+namespace TagHierarchyManager.UI;
+
+public class TagDatabaseService : ObservableObject
+{
+ public event EventHandler DatabaseClosing;
+ public event EventHandler InitialisationComplete;
+
+ public event EventHandler TagAdded;
+ public event EventHandler<(int id, string name)> TagDeleted;
+ public event EventHandler> TagsAdded;
+ public event EventHandler TagUpdated;
+
+ public string DatabaseName => this.Database?.Name ?? string.Empty;
+ public int DatabaseVersion => this.Database?.Version ?? 0;
+ public List DefaultTagBindings => this.Database.DefaultTagBindings;
+
+ public bool IsDatabaseOpen => this.Database != null;
+
+ public int TagCount => this.Database?.Tags.Count ?? 0;
+ public int TagRelationshipCount => this.Database?.GetTagRelationshipCount() ?? 0;
+
+ private TagDatabase? Database { get; set; }
+
+ public void CloseDatabase()
+ {
+ if (this.Database == null) return;
+ this.DatabaseClosing.Invoke(this, EventArgs.Empty);
+ this.Database = null;
+ this.UnsubscribeFromEvents();
+ this.NotifyDatabasePropertiesChanged();
+ }
+
+ public async Task CreateNewDatabase(string filePath, string? templateFilePath = null)
+ {
+ if (this.Database != null)
+ this.CloseDatabase();
+
+ Dictionary? tagsToImport = null;
+ TagDatabase db = new();
+ db.InitialisationComplete += this.TagDatabase_OnInitialisationComplete;
+
+ try
+ {
+ if (string.IsNullOrWhiteSpace(filePath)) return;
+
+ if (templateFilePath is not null && File.Exists(templateFilePath))
+ {
+ var importer = this.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, tagsToImport));
+ }
+ catch (Exception ex)
+ {
+ db.InitialisationComplete -= this.TagDatabase_OnInitialisationComplete;
+ throw;
+ }
+ }
+
+ public async Task DeleteTag(int id)
+ {
+ if (this.Database is null) return;
+ await Task.Run(() => this.Database.DeleteTag(id));
+ }
+
+ public async Task ExportAsync(string path)
+ {
+ if (!this.IsDatabaseOpen || string.IsNullOrWhiteSpace(path)) return;
+ var exporter = PickExporterFromFileExt(path);
+ var exportContent = await Task.Run(() => exporter.ExportDatabase(this.Database!));
+ await File.WriteAllTextAsync(path, exportContent);
+ }
+
+ public List GetAllTagChildren(int id)
+ {
+ return this.Database is not null
+ ? this.Database.Tags.Where(t => t.ParentIds.Contains(id)).OrderBy(t => t.Name).ToList()
+ : [];
+ }
+
+ public List GetAllTags(bool topLevelOnly = false)
+ {
+ if (this.Database is null) return [];
+
+ return topLevelOnly
+ ? this.Database.Tags.Where(t => t.IsTopLevel).OrderBy(t => t.Name).ToList()
+ : this.Database.Tags.OrderBy(t => t.Name).ToList();
+ }
+
+ public List GetParentNamesByIds(List ids)
+ {
+ return ids.Select(id => this.Database?.Tags.FirstOrDefault(t => t.Id == id))
+ .Where(tag => tag is not null)
+ .Select(tag => tag!.Name)
+ .ToList();
+ }
+
+ public async Task LoadDatabase(string filePath)
+ {
+ try
+ {
+ if (this.Database != null)
+ this.CloseDatabase();
+
+ TagDatabase db = new();
+ db.InitialisationComplete += this.TagDatabase_OnInitialisationComplete;
+ await Task.Run(() => db.LoadAsync(filePath));
+ }
+ catch (Exception ex)
+ {
+ // this.ShowErrorDialog(ex.Message);
+ }
+ }
+
+ public List SearchTags(string searchQuery, TagDatabaseSearchMode mode, bool searchAliases)
+ {
+ if (this.Database is null) return [];
+ return searchAliases
+ ? this.Database.SearchWithAliases(searchQuery, mode)
+ : this.Database.Search(searchQuery, mode);
+ }
+
+ public void SetDefaultTagBindings(string input)
+ {
+ this.Database?.DefaultTagBindings = input.Split(";",
+ StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
+ this.NotifyDatabasePropertiesChanged();
+ }
+
+ public async Task WriteTagsToDatabase(List tags)
+ {
+ if (this.Database is null) return;
+ await this.Database.WriteTagsToDatabase(tags);
+ }
+
+ private static IExporter PickExporterFromFileExt(string path)
+ {
+ var fileExt = Path.GetExtension(path);
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if (fileExt == FileTypes.MusicBeeTagHierarchyTemplate.FileExtension) return new MusicBeeTagHierarchyExporter();
+
+ throw new NotSupportedException($"File extension '{fileExt}' is not supported.");
+ }
+
+ private void NotifyDatabasePropertiesChanged()
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ this.OnPropertyChanged(nameof(this.TagCount));
+ this.OnPropertyChanged(nameof(this.DefaultTagBindings));
+ this.OnPropertyChanged(nameof(this.DatabaseVersion));
+ this.OnPropertyChanged(nameof(this.TagRelationshipCount));
+
+ this.OnPropertyChanged(nameof(this.IsDatabaseOpen));
+ this.OnPropertyChanged(nameof(this.DatabaseName));
+ });
+ }
+
+ private Importer PickImporterFromFileExt(string path)
+ {
+ var 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));
+ }
+
+ private void SubscribeToEvents()
+ {
+ if (this.Database is null) return;
+ this.Database.TagAdded += this.TagDatabase_OnTagAdded;
+ this.Database.TagsAdded += this.TagDatabase_OnTagsAdded;
+ this.Database.TagUpdated += this.TagDatabase_OnTagUpdated;
+ this.Database.TagDeleted += this.TagDatabase_OnTagDeleted;
+ }
+
+ // TODO this isn't activating and new database results in NullReferenceException.
+ private void TagDatabase_OnInitialisationComplete(object? sender, EventArgs e)
+ {
+ this.Database = sender as TagDatabase;
+ this.Database.InitialisationComplete -= this.TagDatabase_OnInitialisationComplete;
+ this.SubscribeToEvents();
+ this.NotifyDatabasePropertiesChanged();
+ this.InitialisationComplete.Invoke(this, e);
+ }
+
+ private void TagDatabase_OnTagAdded(object? sender, Tag newTag)
+ {
+ this.TagAdded.Invoke(this, newTag);
+ this.NotifyDatabasePropertiesChanged();
+ }
+
+ private void TagDatabase_OnTagDeleted(object? sender, (int id, string name) deletedTag)
+ {
+ this.TagDeleted.Invoke(this, deletedTag);
+ this.NotifyDatabasePropertiesChanged();
+ }
+
+ private void TagDatabase_OnTagsAdded(object? sender, List newTags)
+ {
+ this.TagsAdded.Invoke(this, newTags);
+ this.NotifyDatabasePropertiesChanged();
+ }
+
+ private void TagDatabase_OnTagUpdated(object? sender, Tag updatedTag)
+ {
+ this.TagUpdated.Invoke(this, updatedTag);
+ this.NotifyDatabasePropertiesChanged();
+ }
+
+ private void UnsubscribeFromEvents()
+ {
+ if (this.Database is null) return;
+ this.Database.TagAdded -= this.TagDatabase_OnTagAdded;
+ this.Database.TagsAdded -= this.TagDatabase_OnTagsAdded;
+ this.Database.TagUpdated -= this.TagDatabase_OnTagUpdated;
+ this.Database.TagDeleted -= this.TagDatabase_OnTagDeleted;
+ }
+}
\ No newline at end of file
diff --git a/UI/TagHierarchyManager.UI.csproj b/UI/TagHierarchyManager.UI.csproj
index fdf0889..de15469 100644
--- a/UI/TagHierarchyManager.UI.csproj
+++ b/UI/TagHierarchyManager.UI.csproj
@@ -20,6 +20,11 @@
True
Resources.resx
+
+
+ UnsavedCancelDialog.axaml
+ Code
+
diff --git a/UI/ViewModels/BulkAddViewModel.cs b/UI/ViewModels/BulkAddViewModel.cs
index 72708ec..a678849 100644
--- a/UI/ViewModels/BulkAddViewModel.cs
+++ b/UI/ViewModels/BulkAddViewModel.cs
@@ -12,21 +12,21 @@ namespace TagHierarchyManager.UI.ViewModels;
public partial class BulkAddViewModel : ViewModelBase
{
+ private readonly MainWindowViewModel _mainWindow;
[ObservableProperty] private ObservableCollection _tags = [new() { Name = "", IsTopLevel = false }];
[ObservableProperty] private string _windowTitle;
public BulkAddViewModel(MainWindowViewModel mainWindow)
{
- this.MainWindow = mainWindow;
+ this._mainWindow = mainWindow;
this.Tags.CollectionChanged += this.OnTagsCollectionChanged;
- this.WindowTitle = string.Format("{0} - " + Resources.ButtonBulkAdd, this.MainWindow.Database?.Name);
+ this.WindowTitle = string.Format("{0} - " + Resources.ButtonBulkAdd,
+ this._mainWindow.TagDatabaseService.DatabaseName);
}
public bool CanSave => this.Tags.Count > 0;
- private MainWindowViewModel MainWindow { get; }
-
public void AddTag()
{
this.Tags.Add(new TagRow());
@@ -39,7 +39,7 @@ public partial class BulkAddViewModel : ViewModelBase
public async Task SaveTags()
{
- if (this.MainWindow.Database is null) return;
+ if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen) return;
var tags = this.Tags.Select(tagRow =>
{
var tag = new Tag
@@ -68,8 +68,8 @@ public partial class BulkAddViewModel : ViewModelBase
})
.ToList();
- await this.MainWindow.Database.WriteTagsToDatabase(tags);
- this.MainWindow.StatusBlockText = string.Format(Resources.StatusBlockBulkAddSuccess, tags.Count);
+ await this._mainWindow.TagDatabaseService.WriteTagsToDatabase(tags);
+ this._mainWindow.StatusBlockText = string.Format(Resources.StatusBlockBulkAddSuccess, tags.Count);
}
private void OnTagsCollectionChanged(object? sender,
diff --git a/UI/ViewModels/DatabaseSettingsViewModel.cs b/UI/ViewModels/DatabaseSettingsViewModel.cs
index 41c5650..30c7693 100644
--- a/UI/ViewModels/DatabaseSettingsViewModel.cs
+++ b/UI/ViewModels/DatabaseSettingsViewModel.cs
@@ -1,12 +1,12 @@
-using System;
-using System.Linq;
-using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
using TagHierarchyManager.UI.Assets;
namespace TagHierarchyManager.UI.ViewModels;
public partial class DatabaseSettingsViewModel : ViewModelBase
{
+ private readonly MainWindowViewModel _mainWindow;
+
[ObservableProperty] private string _databaseVersion;
[ObservableProperty] private string _defaultTagBindings;
[ObservableProperty] private string _tagRowsCount;
@@ -15,21 +15,20 @@ public partial class DatabaseSettingsViewModel : ViewModelBase
public DatabaseSettingsViewModel(MainWindowViewModel mainWindow)
{
- this.MainWindow = mainWindow;
+ this._mainWindow = mainWindow;
- this.WindowTitle = mainWindow.Database!.Name;
- this.DatabaseVersion = string.Format(Resources.DatabaseSettingsDbVersion, mainWindow.Database!.Version);
- var tagRelationshipCount = mainWindow.Database!.GetTagRelationshipCount();
+ this.WindowTitle = mainWindow.TagDatabaseService.DatabaseName;
+ this.DatabaseVersion = string.Format(Resources.DatabaseSettingsDbVersion,
+ mainWindow.TagDatabaseService.DatabaseVersion);
+ var tagRelationshipCount = mainWindow.TagDatabaseService.TagRelationshipCount;
this.TagRowsCount = string.Format(Resources.DatabaseSettingsTagCount,
- mainWindow.Database!.Tags.Count, tagRelationshipCount);
- this.DefaultTagBindings = string.Join("; ", mainWindow.Database!.DefaultTagBindings);
+ mainWindow.TagDatabaseService.TagCount, tagRelationshipCount);
+ this.DefaultTagBindings = string.Join("; ", mainWindow.TagDatabaseService.DefaultTagBindings);
}
- private MainWindowViewModel MainWindow { get; }
public void SaveSettings()
{
- this.MainWindow.Database!.DefaultTagBindings = this.DefaultTagBindings.Split(";",
- StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
+ this._mainWindow.TagDatabaseService.SetDefaultTagBindings(this.DefaultTagBindings);
}
}
\ No newline at end of file
diff --git a/UI/ViewModels/HierarchyTreeViewModel.cs b/UI/ViewModels/HierarchyTreeViewModel.cs
index 24ca808..6c67958 100644
--- a/UI/ViewModels/HierarchyTreeViewModel.cs
+++ b/UI/ViewModels/HierarchyTreeViewModel.cs
@@ -24,41 +24,40 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable
public HierarchyTreeViewModel(MainWindowViewModel mainWindow)
{
this._mainWindow = mainWindow;
- this._getParentNamesById = parentIds =>
- parentIds.Select(id => this._mainWindow.Database?.Tags.FirstOrDefault(t => t.Id == id))
- .Where(tag => tag is not null)
- .Select(tag => tag!.Name)
- .ToList();
+ this._getParentNamesById = mainWindow.TagDatabaseService.GetParentNamesByIds;
this.SubscribeToEvents();
}
public void Dispose()
{
- if (this._mainWindow.Database is null) return;
+ if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen) return;
- this._mainWindow.Database.TagUpdated -= this.TagDatabase_OnTagUpdated;
- this._mainWindow.Database.TagAdded -= this.TagDatabase_OnTagAdded;
- this._mainWindow.Database.TagDeleted -= this.TagDatabase_OnTagDeleted;
- this._mainWindow.Database.TagsAdded -= this.TagDatabase_OnTagsAdded;
+ this._mainWindow.TagDatabaseService.TagUpdated -= this.TagDatabase_OnTagUpdated;
+ this._mainWindow.TagDatabaseService.TagAdded -= this.TagDatabase_OnTagAdded;
+ this._mainWindow.TagDatabaseService.TagDeleted -= this.TagDatabase_OnTagDeleted;
+ this._mainWindow.TagDatabaseService.TagsAdded -= this.TagDatabase_OnTagsAdded;
GC.SuppressFinalize(this);
}
public async Task InitializeAsync()
{
- if (this._mainWindow.Database is null) return;
+ if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen) return;
this.ViewModelMap.Clear();
+ this.TopLevelTagNodes.Clear();
await Task.Run(() =>
{
- var topLevelTags = this._mainWindow.Database?.Tags.Where(t => t.IsTopLevel).ToList();
+ var topLevelTags = this._mainWindow.TagDatabaseService.GetAllTags(true);
var buildingTopLevelNodes = new List();
- foreach (var tagNode in topLevelTags!.Select(tag => new TagItemViewModel(tag, this._getParentNamesById)))
+ foreach (var tagNode in
+ topLevelTags!.Select(tag => new TagItemViewModel(tag, this._getParentNamesById)))
{
buildingTopLevelNodes.Add(tagNode);
this.AddTagNodeToViewModelMap(tagNode);
this.AddAllChildrenAsync(tagNode);
}
- this.TopLevelTagNodes = new ObservableCollection(buildingTopLevelNodes);
+ foreach (var tagNode in buildingTopLevelNodes)
+ this.TopLevelTagNodes.Add(tagNode);
});
}
@@ -72,7 +71,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable
}
var childTags =
- this._mainWindow.Database?.Tags.Where(t => t.ParentIds.Contains(tag.Id)).OrderBy(t => t.Name).ToList();
+ this._mainWindow.TagDatabaseService.GetAllTagChildren(tag.Id);
if (childTags is null) return;
foreach (var childNode in childTags.Select(child =>
@@ -138,7 +137,14 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable
var newTopLevelTag = new TagItemViewModel(tag, this._getParentNamesById);
// this does create considerable delay, would be nice to speed it up somehow.
this.AddAllChildrenAsync(newTopLevelTag);
- this.TopLevelTagNodes.Add(newTopLevelTag);
+
+ var index = 0;
+ while (index < this.TopLevelTagNodes.Count
+ && string.Compare(this.TopLevelTagNodes[index].Name, newTopLevelTag.Name,
+ StringComparison.CurrentCultureIgnoreCase) < 0)
+ index++;
+
+ this.TopLevelTagNodes.Insert(index, newTopLevelTag);
this.AddTagNodeToViewModelMap(newTopLevelTag);
});
}
@@ -186,11 +192,11 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable
private void SubscribeToEvents()
{
- if (this._mainWindow.Database is null) return;
- this._mainWindow.Database.TagUpdated += this.TagDatabase_OnTagUpdated;
- this._mainWindow.Database.TagAdded += this.TagDatabase_OnTagAdded;
- this._mainWindow.Database.TagDeleted += this.TagDatabase_OnTagDeleted;
- this._mainWindow.Database.TagsAdded += this.TagDatabase_OnTagsAdded;
+ if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen) return;
+ this._mainWindow.TagDatabaseService.TagUpdated += this.TagDatabase_OnTagUpdated;
+ this._mainWindow.TagDatabaseService.TagAdded += this.TagDatabase_OnTagAdded;
+ this._mainWindow.TagDatabaseService.TagDeleted += this.TagDatabase_OnTagDeleted;
+ this._mainWindow.TagDatabaseService.TagsAdded += this.TagDatabase_OnTagsAdded;
}
private async void TagDatabase_OnTagAdded(object? sender, Tag newTag)
diff --git a/UI/ViewModels/ImportDialogViewModel.cs b/UI/ViewModels/ImportDialogViewModel.cs
index 27e5599..07fd815 100644
--- a/UI/ViewModels/ImportDialogViewModel.cs
+++ b/UI/ViewModels/ImportDialogViewModel.cs
@@ -1,6 +1,10 @@
using System;
using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
using TagHierarchyManager.UI.Assets;
namespace TagHierarchyManager.UI.ViewModels;
@@ -32,12 +36,15 @@ public partial class ImportDialogViewModel(MainWindowViewModel mainWindow) : Vie
private MainWindowViewModel MainWindow { get; } = mainWindow;
- public async Task InitiateImport()
+ [RelayCommand]
+ private async Task InitiateImport()
{
try
{
+ if (string.IsNullOrEmpty(this.DatabaseFilePath) || string.IsNullOrEmpty(this.TemplateFilePath)) return;
+
this.ImportStatus = Resources.ImportStatusInProgress;
- await this.MainWindow.CreateNewDatabase(this.DatabaseFilePath, this.TemplateFilePath);
+ await this.MainWindow.TagDatabaseService.CreateNewDatabase(this.DatabaseFilePath, this.TemplateFilePath);
this.RequestClose?.Invoke();
}
catch (Exception ex)
@@ -47,4 +54,70 @@ public partial class ImportDialogViewModel(MainWindowViewModel mainWindow) : Vie
error.ShowDialog();
}
}
+
+ [RelayCommand]
+ private async Task ShowDatabaseBrowseDialog()
+ {
+ try
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow is null) return;
+
+ var userWantsToSave = await this.MainWindow.ShowUnsavedChangesDialog();
+ if (userWantsToSave is null) return;
+
+ var storageProvider = desktop.MainWindow?.StorageProvider;
+ if (storageProvider is null) return;
+
+ var file = await storageProvider.SaveFilePickerAsync(
+ new FilePickerSaveOptions
+ {
+ Title = Resources.DialogTitleSaveDatabaseAs,
+ FileTypeChoices = [Common.TagDatabaseFileType]
+ });
+ var path = file?.TryGetLocalPath();
+ if (path == null) return;
+ this.DatabaseFilePath = path;
+ }
+ catch (Exception ex)
+ {
+ this.TemplateFilePath = string.Empty;
+ var error = new ErrorDialogViewModel(ex.Message);
+ error.ShowDialog();
+ }
+ }
+
+ [RelayCommand]
+ private async Task ShowTemplateBrowseDialog()
+ {
+ try
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow is null) return;
+
+ var userWantsToSave = await this.MainWindow.ShowUnsavedChangesDialog();
+ if (userWantsToSave is null) return;
+
+ var storageProvider = desktop.MainWindow?.StorageProvider;
+ if (storageProvider is null) return;
+
+ var files = await storageProvider.OpenFilePickerAsync(
+ new FilePickerOpenOptions
+ {
+ AllowMultiple = false,
+ Title = Resources.DialogTitleChooseImportTemplate,
+ FileTypeFilter = [Common.MusicBeeTagHierarchy]
+ });
+ if (files.Count == 0) return;
+ var path = files[0].TryGetLocalPath();
+ if (path == null) return;
+ this.TemplateFilePath = path;
+ }
+ catch (Exception ex)
+ {
+ this.TemplateFilePath = string.Empty;
+ 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 46bea4d..1b36077 100644
--- a/UI/ViewModels/MainWindowViewModel.cs
+++ b/UI/ViewModels/MainWindowViewModel.cs
@@ -1,30 +1,27 @@
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
using TagHierarchyManager.Common;
-using TagHierarchyManager.Exporters;
-using TagHierarchyManager.Importers;
using TagHierarchyManager.Models;
using TagHierarchyManager.UI.Assets;
using TagHierarchyManager.UI.Views;
namespace TagHierarchyManager.UI.ViewModels;
+// this has become a "god" view model and becoming unwieldy to manage.
public partial class MainWindowViewModel : ViewModelBase
{
- internal TagDatabase? Database;
-
[ObservableProperty] private HierarchyTreeViewModel? _hierarchyTreeViewModel;
[ObservableProperty] private bool _isDbEnabled;
+
private bool _isSwitching;
[ObservableProperty] private SearchViewModel? _searchViewModel;
@@ -34,13 +31,23 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty] private string _statusBlockText = Resources.StatusBlockReady;
+ [ObservableProperty] private TagDatabaseService _tagDatabaseService;
+
[ObservableProperty] private bool _unsavedChanges;
- public int TotalTags => this.Database?.Tags.Count ?? 0;
+ public MainWindowViewModel(TagDatabaseService tagDatabaseService)
+ {
+ this.TagDatabaseService = tagDatabaseService;
+ this.TagDatabaseService.InitialisationComplete += this.TagDatabaseService_OnInitalisationComplete;
+ this.TagDatabaseService.TagAdded += this.TagDatabaseService_TagAdded;
+ this.TagDatabaseService.TagDeleted += this.TagDatabaseService_TagDeleted;
+ }
+
+ public int TotalTags => this.TagDatabaseService.TagCount;
public string WindowTitle =>
- this.Database is not null
- ? string.Format(Resources.TitleWithDatabase, this.Database?.Name)
+ this.TagDatabaseService.IsDatabaseOpen
+ ? string.Format(Resources.TitleWithDatabase, this.TagDatabaseService.DatabaseName)
: Resources.Title;
public TagItemViewModel? SelectedTag
@@ -67,85 +74,29 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
- 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
- {
- if (string.IsNullOrWhiteSpace(filePath)) return;
-
- if (templateFilePath is not null && File.Exists(templateFilePath))
- {
- var importer = this.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, tagsToImport));
- }
- catch (Exception ex)
- {
- db.InitialisationComplete -= this.TagDatabase_OnInitalisationComplete;
- if (templateFilePath is null) this.ShowErrorDialog(ex.Message);
- else throw;
- }
- }
-
- public async Task ExportAsync(string path)
+ public async Task NewTag()
{
- if (this.Database is null || string.IsNullOrWhiteSpace(path)) return;
- this.IsDbEnabled = false;
- var exporter = PickExporterFromFileExt(path);
- this.StatusBlockText = Resources.StatusBlockExportInProgress;
- var exportContent = await Task.Run(() => exporter.ExportDatabase(this.Database!));
- await File.WriteAllTextAsync(path, exportContent);
- this.IsDbEnabled = true;
- this.StatusBlockText = string.Format(Resources.StatusBlockExportSuccessful, path);
- }
+ var userWantsToSave = await this.ShowUnsavedChangesDialog();
+ if (userWantsToSave is null) return;
- public async Task LoadDatabase(string filePath)
- {
- try
- {
- if (this.Database != null)
- this.UninitialiseDatabase();
-
- TagDatabase db = new();
- db.InitialisationComplete += this.TagDatabase_OnInitalisationComplete;
- await Task.Run(() => db.LoadAsync(filePath));
- }
- catch (Exception ex)
- {
- this.ShowErrorDialog(ex.Message);
- }
- }
-
- public void NewTag()
- {
this.SelectedTag = new TagItemViewModel(
new Tag
{
Name = string.Empty,
IsTopLevel = true,
- TagBindings = this.Database!.DefaultTagBindings
+ TagBindings = this.TagDatabaseService.DefaultTagBindings
}
);
this.UnsavedChanges = true;
}
+ [RelayCommand]
public async Task SaveSelectedTagAsync()
{
- if (this.SelectedTag is null || this.Database is null) return;
+ if (this.SelectedTag is null || !this.TagDatabaseService.IsDatabaseOpen || !this.IsDbEnabled) return;
this.SelectedTag.CommitEdit();
- await this.Database.WriteTagToDatabase(this.SelectedTag.Tag);
+ await this.TagDatabaseService.WriteTagsToDatabase([this.SelectedTag.Tag]);
this.SelectedTag.RefreshParentsString();
this.StatusBlockText = string.Format(Resources.StatusBlockTagSaveSuccessful, this.SelectedTag.Name);
this.UnsavedChanges = false;
@@ -200,42 +151,57 @@ public partial class MainWindowViewModel : ViewModelBase
return result;
}
+ public async Task ShowUnsavedChangesDialog()
+ {
+ if (!this.UnsavedChanges) return true;
+ var result = await this.ShowNullableBoolDialog(new UnsavedChangesDialog());
+ return result;
+ }
+
public void StartSearch(string searchQuery, TagDatabaseSearchMode mode, bool searchAliases)
{
this.SearchViewModel?.Search(searchQuery, mode, searchAliases);
}
- public async Task StartTagDeletion()
+ [RelayCommand]
+ private async Task CancelTagEditAsync()
{
- var result = await this.ShowNullableBoolDialog(new DeleteTagDialog());
- if (result == null)
- return;
+ if (this.UnsavedChanges)
+ {
+ var userWantsToOverwrite = await this.ShowNullableBoolDialog(new UnsavedCancelDialog());
+ if (userWantsToOverwrite is not null && (bool)!userWantsToOverwrite) return;
+ }
- if (result == true) await this.DeleteSelectedTagAsync();
+ this.SelectedTag?.BeginEdit();
+ this.UnsavedChanges = false;
}
- private static IExporter PickExporterFromFileExt(string path)
+ private void CloseDatabase()
{
- var fileExt = Path.GetExtension(path);
-
- // ReSharper disable once ConvertIfStatementToReturnStatement
- if (fileExt == FileTypes.MusicBeeTagHierarchyTemplate.FileExtension) return new MusicBeeTagHierarchyExporter();
-
- throw new NotSupportedException($"File extension '{fileExt}' is not supported.");
+ this.SelectedTag = null;
+ this.IsDbEnabled = false;
+ this.HierarchyTreeViewModel = null;
+ this.SearchViewModel = null;
+ this.OnPropertyChanged(nameof(this.TotalTags));
+ this.OnPropertyChanged(nameof(this.WindowTitle));
+ this.TagDatabaseService.CloseDatabase();
}
private async Task DeleteSelectedTagAsync()
{
- if (this.SelectedTag is null || this.Database is null) return;
+ var oldTag = this.SelectedTag;
+ if (this.SelectedTag is null || this.TagDatabaseService.IsDatabaseOpen) return;
try
{
- await this.Database.DeleteTag(this.SelectedTag.Tag.Id);
+ await this.TagDatabaseService.DeleteTag(this.SelectedTag.Tag.Id);
this._selectedTag = null;
this.HierarchyTreeViewModel?.SelectedTag = null;
this.OnPropertyChanged(nameof(this.SelectedTag));
}
catch (Exception ex)
{
+ this._selectedTag = oldTag;
+ // TODO invoke deleting/delete error so hierarchy tree can recover
this.ShowErrorDialog(ex.Message);
}
}
@@ -247,18 +213,23 @@ public partial class MainWindowViewModel : ViewModelBase
{
var result = await this.ShowNullableBoolDialog(new UnsavedChangesDialog());
- if (result == null)
+ switch (result)
{
- this._selectedTag = oldTag;
- this.HierarchyTreeViewModel?.SelectedTag = oldTag;
- this.OnPropertyChanged(nameof(this.SelectedTag));
- return;
+ case null:
+ this._selectedTag = oldTag;
+ this.HierarchyTreeViewModel?.SelectedTag = oldTag;
+ this.OnPropertyChanged(nameof(this.SelectedTag));
+ return;
+ case false:
+ break;
+ case true:
+ await this.SaveSelectedTagAsync();
+ break;
}
- if (result == true)
- await this.SaveSelectedTagAsync();
-
-
+ // do not set this to the generated public variable from Avalonia.
+ // OnPropertyChanged must be handled manually here to prevent tag changes firing
+ // twice.
this._selectedTag = newTag;
this.HierarchyTreeViewModel?.SelectedTag = newTag;
this._selectedTag?.BeginEdit();
@@ -272,21 +243,74 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
+
+ [RelayCommand]
+ private async Task NewDatabaseAsync()
+ {
+ try
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow is null) return;
+
+ var userWantsToSave = await this.ShowUnsavedChangesDialog();
+ if (userWantsToSave is null) return;
+
+ var storageProvider = desktop.MainWindow?.StorageProvider;
+ if (storageProvider is null) return;
+
+ var file = await storageProvider.SaveFilePickerAsync(
+ new FilePickerSaveOptions
+ {
+ Title = Resources.DialogTitleSaveDatabaseAs,
+ FileTypeChoices = [Common.TagDatabaseFileType]
+ });
+ var path = file?.TryGetLocalPath();
+ if (path == null) return;
+ await this.TagDatabaseService.CreateNewDatabase(path);
+ }
+ catch (Exception ex)
+ {
+ var error = new ErrorDialogViewModel(ex.Message);
+ error.ShowDialog();
+ }
+ }
+
private void OnUserEditedTag(object? sender, EventArgs e)
{
this.UnsavedChanges = true;
}
- private Importer PickImporterFromFileExt(string path)
+ [RelayCommand]
+ private async Task OpenDatabase()
{
- var fileExt = Path.GetExtension(path);
+ try
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow is null) return;
- // 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();
+ var userWantsToSave = await this.ShowUnsavedChangesDialog();
+ if (userWantsToSave is null) return;
- throw new NotSupportedException(string.Format(Resources.ErrorImportFileTypeNotSupported, fileExt));
+ var storageProvider = desktop.MainWindow?.StorageProvider;
+ if (storageProvider is null) return;
+
+ var files = await storageProvider.OpenFilePickerAsync(
+ new FilePickerOpenOptions
+ {
+ AllowMultiple = false,
+ Title = Resources.DialogTitleOpenDatabase,
+ FileTypeFilter = [Common.TagDatabaseFileType]
+ });
+ if (files.Count == 0) return;
+ var path = files[0].TryGetLocalPath();
+ if (path == null) return;
+ await this.TagDatabaseService.LoadDatabase(path);
+ }
+ catch (Exception ex)
+ {
+ var error = new ErrorDialogViewModel(ex.Message);
+ error.ShowDialog();
+ }
}
private void ShowErrorDialog(string message)
@@ -295,9 +319,63 @@ public partial class MainWindowViewModel : ViewModelBase
error.ShowDialog();
}
- private void TagDatabase_OnInitalisationComplete(object? sender, EventArgs e)
+ [RelayCommand]
+ private async Task StartExportAsync()
+ {
+ if (!this.TagDatabaseService.IsDatabaseOpen ||
+ Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow is null) return;
+
+ var userWantsToSave = await this.ShowUnsavedChangesDialog();
+ if (userWantsToSave is null) return;
+
+ var storageProvider = desktop.MainWindow?.StorageProvider;
+ if (storageProvider is null) return;
+
+ try
+ {
+ var file = await storageProvider.SaveFilePickerAsync(
+ new FilePickerSaveOptions
+ {
+ Title = Resources.DialogTitleExportTagDatabase,
+ FileTypeChoices = [Common.MusicBeeTagHierarchy],
+ SuggestedFileName = this.TagDatabaseService.DatabaseName
+ });
+ var path = file?.TryGetLocalPath();
+ if (path == null) return;
+ this.IsDbEnabled = false;
+ this.StatusBlockText = Resources.StatusBlockExportInProgress;
+ await this.TagDatabaseService.ExportAsync(path);
+ this.IsDbEnabled = true;
+ this.StatusBlockText = string.Format(Resources.StatusBlockExportSuccessful, path);
+ }
+ catch (Exception ex)
+ {
+ this.IsDbEnabled = true;
+ var error = new ErrorDialogViewModel(ex.Message);
+ error.ShowDialog();
+ }
+ }
+
+ [RelayCommand]
+ private async Task StartTagDeletionAsync()
{
- if (sender is not TagDatabase db) return;
+ if (this.SelectedTag is null || this.SelectedTag.HasChildren) return;
+ var result = await this.ShowNullableBoolDialog(new DeleteTagDialog());
+ switch (result)
+ {
+ case null:
+ case false:
+ return;
+ case true:
+ await this.DeleteSelectedTagAsync();
+ break;
+ }
+ }
+
+ private void TagDatabaseService_OnInitalisationComplete(object? sender, EventArgs e)
+ {
+ if (sender is not TagDatabaseService tdb) return;
Dispatcher.UIThread.Post(async void () =>
{
try
@@ -305,59 +383,36 @@ public partial class MainWindowViewModel : ViewModelBase
(this.HierarchyTreeViewModel as IDisposable)?.Dispose();
(this.SearchViewModel as IDisposable)?.Dispose();
- if (this.Database != null)
- {
- this.Database.TagAdded -= this.TagDatabase_TagAdded;
- this.Database.TagDeleted -= this.TagDatabase_TagDeleted;
- }
-
- this.Database = db;
this.SelectedTag = null;
this.HierarchyTreeViewModel = new HierarchyTreeViewModel(this);
this.SearchViewModel = new SearchViewModel(this);
- this.Database.TagAdded += this.TagDatabase_TagAdded;
- this.Database.TagDeleted += this.TagDatabase_TagDeleted;
+
await this.HierarchyTreeViewModel.InitializeAsync();
this.OnPropertyChanged(nameof(this.TotalTags));
this.OnPropertyChanged(nameof(this.WindowTitle));
this.IsDbEnabled = true;
- this.Database.InitialisationComplete -= this.TagDatabase_OnInitalisationComplete;
this.UnsavedChanges = false;
- this.StatusBlockText = string.Format(Resources.StatusBlockDbLoadSuccessful, this.Database.Name);
+ this.StatusBlockText = string.Format(Resources.StatusBlockDbLoadSuccessful,
+ this.TagDatabaseService.DatabaseName);
}
catch (Exception ex)
{
- this.UninitialiseDatabase();
+ this.CloseDatabase();
this.ShowErrorDialog(ex.Message);
}
});
- Debug.WriteLine($"Database loaded on UI - name: {db.Name}, version: {db.Version}");
}
- private void TagDatabase_TagAdded(object? sender, Tag _)
+ private void TagDatabaseService_TagAdded(object? sender, Tag _)
{
this.OnPropertyChanged(nameof(this.TotalTags));
this.SelectedTag!.SyncId();
}
- private void TagDatabase_TagDeleted(object? sender, (int id, string name) _)
+ private void TagDatabaseService_TagDeleted(object? sender, (int id, string name) _)
{
this.OnPropertyChanged(nameof(this.TotalTags));
}
-
- private void UninitialiseDatabase()
- {
- if (this.Database == null) return;
- this.SelectedTag = null;
- this.IsDbEnabled = false;
- this.Database.TagAdded -= this.TagDatabase_TagAdded;
- this.Database.TagDeleted -= this.TagDatabase_TagDeleted;
- this.Database = null;
- this.HierarchyTreeViewModel = null;
- this.SearchViewModel = null;
- this.OnPropertyChanged(nameof(this.TotalTags));
- this.OnPropertyChanged(nameof(this.WindowTitle));
- }
}
\ No newline at end of file
diff --git a/UI/ViewModels/SearchViewModel.cs b/UI/ViewModels/SearchViewModel.cs
index b8bfcbd..7a1c6bf 100644
--- a/UI/ViewModels/SearchViewModel.cs
+++ b/UI/ViewModels/SearchViewModel.cs
@@ -20,28 +20,23 @@ public partial class SearchViewModel : ViewModelBase, IDisposable
public SearchViewModel(MainWindowViewModel mainWindow)
{
this._mainWindow = mainWindow;
- this._getParentNamesById = parents =>
- parents.Select(id => this._mainWindow.Database?.Tags.FirstOrDefault(t => t.Id == id))
- .Where(tag => tag is not null)
- .Select(tag => tag!.Name)
- .ToList();
- mainWindow.Database?.TagDeleted += this.TagDatabase_OnTagDeleted;
+ this._getParentNamesById = mainWindow.TagDatabaseService.GetParentNamesByIds;
+ mainWindow.TagDatabaseService.TagDeleted += this.TagDatabase_OnTagDeleted;
}
public void Dispose()
{
- this._mainWindow.Database?.TagDeleted -= this.TagDatabase_OnTagDeleted;
+ this._mainWindow.TagDatabaseService.TagDeleted -= this.TagDatabase_OnTagDeleted;
}
public void Search(string searchQuery, TagDatabaseSearchMode mode, bool searchAliases)
{
- if (this._mainWindow.Database == null || string.IsNullOrWhiteSpace(searchQuery)) return;
-
- var results = searchAliases
- ? this._mainWindow.Database.SearchWithAliases(searchQuery, mode)
- : this._mainWindow.Database.Search(searchQuery, mode);
+ if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen || string.IsNullOrWhiteSpace(searchQuery)) return;
this.SearchResults.Clear();
+
+ var results = this._mainWindow.TagDatabaseService.SearchTags(searchQuery, mode, searchAliases);
+
if (results.Count == 0)
{
this._mainWindow.StatusBlockText = Resources.SearchNoResultsFound;
diff --git a/UI/ViewModels/TagItemViewModel.cs b/UI/ViewModels/TagItemViewModel.cs
index 90e92f0..0473ca0 100644
--- a/UI/ViewModels/TagItemViewModel.cs
+++ b/UI/ViewModels/TagItemViewModel.cs
@@ -148,13 +148,13 @@ public partial class TagItemViewModel(Tag tag, Func, List>? ge
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
- if (string.IsNullOrWhiteSpace(this.Name))
+ if (string.IsNullOrWhiteSpace(this.EditingName))
throw new ArgumentException(Resources.ErrorBlankTagName);
if (!this.EditingIsTopLevel && parentNames.Count == 0)
throw new InvalidOperationException(Resources.ErrorOrphanTagAttempt);
- if (parentNames.Contains(this.Name))
+ if (parentNames.Contains(this.EditingName))
throw new InvalidOperationException(Resources.ErrorSelfParentAttempt);
}
}
\ No newline at end of file
diff --git a/UI/Views/BulkAddWindow.axaml.cs b/UI/Views/BulkAddWindow.axaml.cs
index 7b93462..0ae4d85 100644
--- a/UI/Views/BulkAddWindow.axaml.cs
+++ b/UI/Views/BulkAddWindow.axaml.cs
@@ -8,6 +8,7 @@ namespace TagHierarchyManager.UI.Views;
public partial class BulkAddWindow : Window
{
+ // TODO keydown del = delete selected row/s
public BulkAddWindow()
{
this.InitializeComponent();
diff --git a/UI/Views/Common.cs b/UI/Views/Common.cs
index 0aa25e2..3587328 100644
--- a/UI/Views/Common.cs
+++ b/UI/Views/Common.cs
@@ -1,7 +1,7 @@
using Avalonia.Platform.Storage;
using TagHierarchyManager.UI.Assets;
-namespace TagHierarchyManager.UI.Views;
+namespace TagHierarchyManager.UI;
public static class Common
{
diff --git a/UI/Views/ImportDialog.axaml b/UI/Views/ImportDialog.axaml
index 1fe9523..48a0e44 100644
--- a/UI/Views/ImportDialog.axaml
+++ b/UI/Views/ImportDialog.axaml
@@ -17,7 +17,8 @@
Foreground="{DynamicResource SemiColorTertiary}"
Text="{Binding VisibleTemplateFilePath}" />
+ Content="{x:Static assets:Resources.ButtonBrowse}"
+ Command="{Binding ShowTemplateBrowseDialogCommand}" />
@@ -26,14 +27,16 @@
Foreground="{DynamicResource SemiColorTertiary}"
Text="{Binding VisibleDatabaseFilePath}" />
+ Content="{x:Static assets:Resources.ButtonBrowse}"
+ Command="{Binding ShowDatabaseBrowseDialogCommand}" />
+ IsEnabled="{Binding BothFilesSelected}"
+ Command="{Binding InitiateImportCommand}" />
\ No newline at end of file
diff --git a/UI/Views/ImportDialog.axaml.cs b/UI/Views/ImportDialog.axaml.cs
index b2fb31c..c10fb44 100644
--- a/UI/Views/ImportDialog.axaml.cs
+++ b/UI/Views/ImportDialog.axaml.cs
@@ -1,7 +1,5 @@
-using System;
-using Avalonia.Controls;
+using Avalonia.Controls;
using Avalonia.Interactivity;
-using Avalonia.Platform.Storage;
using TagHierarchyManager.UI.ViewModels;
namespace TagHierarchyManager.UI.Views;
@@ -16,7 +14,7 @@ public partial class ImportDialog : Window
{
if (this.ViewModel != null) this.ViewModel.RequestClose += this.Close;
};
-
+
this.Unloaded += (_, _) =>
{
if (this.ViewModel != null)
@@ -26,68 +24,8 @@ public partial class ImportDialog : Window
private ImportDialogViewModel? ViewModel => this.DataContext as ImportDialogViewModel;
- public async void ButtonBrowseDatabase_Click(object? sender, RoutedEventArgs e)
+ public void ButtonCancel_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 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 void ButtonCancel_Click(object? sender, RoutedEventArgs e) =>
this.Close();
-
- public async void ButtonImport_Click(object? sender, RoutedEventArgs e)
- {
- try
- {
- if (this.ViewModel?.DatabaseFilePath == null || this.ViewModel?.TemplateFilePath == null ||
- this.ViewModel is null) return;
- await this.ViewModel.InitiateImport();
- }
- catch (Exception ex)
- {
- var error = new ErrorDialogViewModel(ex.Message);
- error.ShowDialog();
- }
}
-
}
\ No newline at end of file
diff --git a/UI/Views/MainWindow.axaml b/UI/Views/MainWindow.axaml
index 896ee25..f40fb50 100644
--- a/UI/Views/MainWindow.axaml
+++ b/UI/Views/MainWindow.axaml
@@ -12,14 +12,15 @@
-
+
+
+
+
+
\ No newline at end of file
diff --git a/UI/Views/MainWindow.axaml.cs b/UI/Views/MainWindow.axaml.cs
index 75c0f06..1408514 100644
--- a/UI/Views/MainWindow.axaml.cs
+++ b/UI/Views/MainWindow.axaml.cs
@@ -2,12 +2,15 @@ using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
-using Avalonia.Platform.Storage;
using TagHierarchyManager.Common;
using TagHierarchyManager.UI.ViewModels;
namespace TagHierarchyManager.UI.Views;
+/*
+ * TODO:
+ * add "recently added" functionality.
+ */
public partial class MainWindow : Window
{
private bool _userWantsToQuit;
@@ -19,55 +22,25 @@ public partial class MainWindow : Window
private MainWindowViewModel? ViewModel => this.DataContext as MainWindowViewModel;
+ // TODO make relay command
public void ButtonAdd_Click(object? sender, RoutedEventArgs e)
{
this.ViewModel?.NewTag();
}
+ // TODO make relay command
public void ButtonBulkAdd_Click(object? sender, RoutedEventArgs e)
{
this.ViewModel?.ShowBulkAddDialog();
}
- public void ButtonCancel_Click(object? sender, RoutedEventArgs e)
- {
- this.ViewModel?.SelectedTag?.BeginEdit();
- }
-
- public void ButtonDelete_Click(object? sender, RoutedEventArgs e)
- {
- try
- {
- if (this.ViewModel?.Database is null) return;
- this.ViewModel?.StartTagDeletion();
- }
- catch (Exception ex)
- {
- var error = new ErrorDialogViewModel(ex.Message);
- error.ShowDialog();
- }
- }
-
- public async void ButtonSave_Click(object? sender, RoutedEventArgs e)
- {
- try
- {
- if (this.ViewModel?.SelectedTag is null) return;
- await this.ViewModel.SaveSelectedTagAsync();
- }
- catch (Exception ex)
- {
- var error = new ErrorDialogViewModel(ex.Message);
- error.ShowDialog();
- }
- }
-
+ // TODO make relay command
public void ButtonSearch_Click(object? sender, RoutedEventArgs e)
{
try
{
- if (this.ViewModel?.Database is null) return;
- this.ViewModel?.StartSearch(
+ if (this.ViewModel is null) return;
+ this.ViewModel.StartSearch(
this.SearchTextBox.Text!,
(TagDatabaseSearchMode)this.SearchModeComboBox.SelectedIndex,
this.SearchAliasesCheckBox.IsChecked ?? false);
@@ -79,99 +52,31 @@ public partial class MainWindow : Window
}
}
+ // TODO make relay command?
public void MenuItemDatabaseSettings_Click(object? sender, RoutedEventArgs e)
{
- if (this.ViewModel?.Database == null) return;
- this.ViewModel.ShowDatabaseSettings();
- }
-
- // Resharper disable once AsyncVoidEventHandlerMethod
- public async void MenuItemExport_Click(object? sender, RoutedEventArgs e)
- {
- if (this.ViewModel?.Database == null) return;
- try
- {
- var file = await this.StorageProvider.SaveFilePickerAsync(
- new FilePickerSaveOptions
- {
- Title = Assets.Resources.DialogTitleExportTagDatabase,
- FileTypeChoices = [Common.MusicBeeTagHierarchy],
- SuggestedFileName = this.ViewModel.Database.Name
- });
- var path = file?.TryGetLocalPath();
- if (path == null) return;
- await this.ViewModel.ExportAsync(path);
- }
- catch (Exception ex)
- {
- this.ViewModel.IsDbEnabled = true;
- var error = new ErrorDialogViewModel(ex.Message);
- error.ShowDialog();
- }
+ this.ViewModel?.ShowDatabaseSettings();
}
+ // TODO make relay command?
public void MenuItemImport_Click(object? sender, RoutedEventArgs e)
{
this.ViewModel?.ShowImportDialog();
}
- public async void MenuItemNew_Click(object? sender, RoutedEventArgs e)
- {
- try
- {
- if (this.ViewModel is null) return;
- var file = await this.StorageProvider.SaveFilePickerAsync(
- new FilePickerSaveOptions
- {
- Title = Assets.Resources.DialogTitleSaveDatabaseAs,
- FileTypeChoices = [Common.TagDatabaseFileType]
- });
- var path = file?.TryGetLocalPath();
- if (path == null) return;
- await this.ViewModel.CreateNewDatabase(path);
- }
- catch (Exception ex)
- {
- var error = new ErrorDialogViewModel(ex.Message);
- error.ShowDialog();
- }
- }
-
- public async void MenuItemOpen_Click(object? sender, RoutedEventArgs e)
- {
- try
- {
- if (this.ViewModel is null) return;
- var files = await this.StorageProvider.OpenFilePickerAsync(
- new FilePickerOpenOptions
- {
- AllowMultiple = false,
- 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);
- }
- catch (Exception ex)
- {
- var error = new ErrorDialogViewModel(ex.Message);
- error.ShowDialog();
- }
- }
-
public void MenuItemQuit_Click(object? sender, RoutedEventArgs e)
{
this.Close();
}
+ // make relay command?
public void OpenAboutWindow(object? sender, RoutedEventArgs e)
{
new AboutWindow().ShowDialog(this);
}
+ // make relay command?
public void SearchTextBox_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
@@ -187,7 +92,7 @@ public partial class MainWindow : Window
if (this.ViewModel is not null && this.ViewModel.UnsavedChanges)
{
e.Cancel = true;
- var result = await this.ViewModel.ShowNullableBoolDialog(new UnsavedChangesDialog());
+ var result = await this.ViewModel.ShowUnsavedChangesDialog();
switch (result)
{
case true:
diff --git a/UI/Views/UnsavedCancelDialog.axaml b/UI/Views/UnsavedCancelDialog.axaml
new file mode 100644
index 0000000..e6d24e3
--- /dev/null
+++ b/UI/Views/UnsavedCancelDialog.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/UI/Views/UnsavedCancelDialog.axaml.cs b/UI/Views/UnsavedCancelDialog.axaml.cs
new file mode 100644
index 0000000..90a34cc
--- /dev/null
+++ b/UI/Views/UnsavedCancelDialog.axaml.cs
@@ -0,0 +1,22 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace TagHierarchyManager.UI.Views;
+
+public partial class UnsavedCancelDialog : Window
+{
+ public UnsavedCancelDialog()
+ {
+ this.InitializeComponent();
+ }
+
+ public void ButtonNo_Click(object? sender, RoutedEventArgs e)
+ {
+ this.Close(false);
+ }
+
+ public void ButtonYes_Click(object? sender, RoutedEventArgs e)
+ {
+ this.Close(true);
+ }
+}
\ No newline at end of file