From 3ee5ca43aa10a1bcf6b2d963ca7e2c04f3646b47 Mon Sep 17 00:00:00 2001 From: Flaky Date: Wed, 14 Jan 2026 15:39:03 +0000 Subject: [PATCH] cleanup before release --- .../TagDatabase/TagDatabase.DeleteMethods.cs | 2 +- Core/TagHierarchyManager.csproj | 1 - UI/ViewModels/ErrorDialogViewModel.cs | 5 +- UI/ViewModels/HierarchyTreeViewModel.cs | 13 +- UI/ViewModels/ImportDialogViewModel.cs | 13 +- UI/ViewModels/MainWindowViewModel.cs | 58 ++---- UI/ViewModels/SearchViewModel.cs | 22 +- UI/ViewModels/TagItemViewModel.cs | 8 +- UI/Views/Common.cs | 2 +- UI/Views/ImportDialog.axaml.cs | 26 ++- UI/Views/MainWindow.axaml | 20 +- UI/Views/MainWindow.axaml.cs | 188 ++++++++++++------ 12 files changed, 217 insertions(+), 141 deletions(-) diff --git a/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs b/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs index 9b4f7ea..d517dcc 100644 --- a/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs @@ -69,7 +69,7 @@ partial class TagDatabase tag.Parents.Remove(targetTag.Name); }); - Tag cachedTag = this.Tags.FirstOrDefault(t => t.Id == targetTag.Id); + Tag? cachedTag = this.Tags.FirstOrDefault(t => t.Id == targetTag.Id); if (cachedTag != null) { this.Tags.Remove(cachedTag); diff --git a/Core/TagHierarchyManager.csproj b/Core/TagHierarchyManager.csproj index 9127e84..7dcc757 100644 --- a/Core/TagHierarchyManager.csproj +++ b/Core/TagHierarchyManager.csproj @@ -6,7 +6,6 @@ enable enable false - true true diff --git a/UI/ViewModels/ErrorDialogViewModel.cs b/UI/ViewModels/ErrorDialogViewModel.cs index f91d0cf..c2272d9 100644 --- a/UI/ViewModels/ErrorDialogViewModel.cs +++ b/UI/ViewModels/ErrorDialogViewModel.cs @@ -26,7 +26,10 @@ public partial class ErrorDialogViewModel : ViewModelBase DataContext = this, Title = Resources.ErrorDialogTitle }; + error.Closed += (_, _) => error.DataContext = null; + var ownerWindow = desktop.Windows.FirstOrDefault(w => w.IsActive) ?? desktop.MainWindow; - error.ShowDialog(ownerWindow); + if (ownerWindow == null) error.Show(); + else error.ShowDialog(ownerWindow); } } \ No newline at end of file diff --git a/UI/ViewModels/HierarchyTreeViewModel.cs b/UI/ViewModels/HierarchyTreeViewModel.cs index b49967f..e33e6f9 100644 --- a/UI/ViewModels/HierarchyTreeViewModel.cs +++ b/UI/ViewModels/HierarchyTreeViewModel.cs @@ -48,12 +48,12 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable viewModel = new TagItemViewModel(tag, id => this._viewModelMap.Values.FirstOrDefault(v => v.Id == id)?.Name); this._viewModelMap[key] = viewModel; - viewModel.UserEditedTag += (s, e) => this._mainWindow.UnsavedChanges = true; + viewModel.UserEditedTag += this.OnUserEditedTag; } return viewModel; } - + partial void OnSelectedTagChanged(TagItemViewModel? value) { this._mainWindow.SelectedTag = value; @@ -67,6 +67,8 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable }); await this.SyncHierarchyAsync(); } + + private void OnUserEditedTag(object? sender, EventArgs e) => this._mainWindow.UnsavedChanges = true; private void SubscribeToEvents() { @@ -95,7 +97,6 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable } } - private async Task SyncHierarchyAsync() { if (this._mainWindow.Database is null) return; @@ -123,7 +124,11 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable this.SyncCollection(this.TopLevelTags, topLevelViewModels); var keysToRemove = this._viewModelMap.Keys.Where(k => !activeKeys.Contains(k)).ToList(); - foreach (var key in keysToRemove) this._viewModelMap.Remove(key); + foreach (var key in keysToRemove) + { + this._viewModelMap[key].UserEditedTag -= this.OnUserEditedTag; + this._viewModelMap.Remove(key); + } } private void SyncTagRecursive(TagItemViewModel parentVm, ILookup childrenLookup, diff --git a/UI/ViewModels/ImportDialogViewModel.cs b/UI/ViewModels/ImportDialogViewModel.cs index c09bc41..27e5599 100644 --- a/UI/ViewModels/ImportDialogViewModel.cs +++ b/UI/ViewModels/ImportDialogViewModel.cs @@ -5,24 +5,19 @@ using TagHierarchyManager.UI.Assets; namespace TagHierarchyManager.UI.ViewModels; -public partial class ImportDialogViewModel : ViewModelBase +public partial class ImportDialogViewModel(MainWindowViewModel mainWindow) : ViewModelBase { [ObservableProperty] [NotifyPropertyChangedFor(nameof(VisibleDatabaseFilePath))] [NotifyPropertyChangedFor(nameof(BothFilesSelected))] - private string _databaseFilePath; + private string _databaseFilePath = string.Empty; [ObservableProperty] private string _importStatus = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(VisibleTemplateFilePath))] [NotifyPropertyChangedFor(nameof(BothFilesSelected))] - private string _templateFilePath; - - public ImportDialogViewModel(MainWindowViewModel mainWindow) - { - this.MainWindow = mainWindow; - } + private string _templateFilePath = string.Empty; public event Action? RequestClose; @@ -35,7 +30,7 @@ public partial class ImportDialogViewModel : ViewModelBase public string VisibleTemplateFilePath => !string.IsNullOrWhiteSpace(this.TemplateFilePath) ? this.TemplateFilePath : Resources.ImportNoFilePicked; - private MainWindowViewModel MainWindow { get; } + private MainWindowViewModel MainWindow { get; } = mainWindow; public async Task InitiateImport() { diff --git a/UI/ViewModels/MainWindowViewModel.cs b/UI/ViewModels/MainWindowViewModel.cs index a14e0f2..92214ec 100644 --- a/UI/ViewModels/MainWindowViewModel.cs +++ b/UI/ViewModels/MainWindowViewModel.cs @@ -42,7 +42,7 @@ public partial class MainWindowViewModel : ViewModelBase public int TotalTags => this.Database?.Tags.Count ?? 0; public string WindowTitle => - this.IsDbEnabled + this.Database is not null ? string.Format(Resources.TitleWithDatabase, this.Database?.Name) : Resources.Title; @@ -52,7 +52,7 @@ public partial class MainWindowViewModel : ViewModelBase set { if (this._selectedTag == value || this._isSwitching) return; - if (!this.IsDbEnabled && value == null) this._selectedTag = value; + if (value == null) this._selectedTag = value; if (this._selectedTag != null && this.UnsavedChanges) { _ = this.HandleTagSwitchAsync(this._selectedTag, value); @@ -101,23 +101,14 @@ public partial class MainWindowViewModel : ViewModelBase public async Task ExportAsync(string path) { + if (this.Database is null || string.IsNullOrWhiteSpace(path)) return; this.IsDbEnabled = false; - try - { - if (this.Database is null || string.IsNullOrWhiteSpace(path)) return; - - 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); - } - catch (Exception ex) - { - this.IsDbEnabled = true; - this.ShowErrorDialog(ex.Message); - } + 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); } public async Task LoadDatabase(string filePath) @@ -153,19 +144,12 @@ public partial class MainWindowViewModel : ViewModelBase public async Task SaveSelectedTagAsync() { if (this.SelectedTag is null || this.Database is null) return; - - try - { - this.SelectedTag.CommitEdit(); - await this.Database.WriteTagToDatabase(this.SelectedTag.Tag); - this.SelectedTag.RefreshParentsString(); - this.StatusBlockText = string.Format(Resources.StatusBlockTagSaveSuccessful, this.SelectedTag.Name); - this.UnsavedChanges = false; - } - catch (Exception ex) - { - this.ShowErrorDialog(ex.Message); - } + + this.SelectedTag.CommitEdit(); + await this.Database.WriteTagToDatabase(this.SelectedTag.Tag); + this.SelectedTag.RefreshParentsString(); + this.StatusBlockText = string.Format(Resources.StatusBlockTagSaveSuccessful, this.SelectedTag.Name); + this.UnsavedChanges = false; } public void ShowDatabaseSettings() @@ -181,12 +165,6 @@ public partial class MainWindowViewModel : ViewModelBase dialog.ShowDialog(desktop.MainWindow!); } - public void ShowErrorDialog(string message) - { - var error = new ErrorDialogViewModel(message); - error.ShowDialog(); - } - public void ShowImportDialog() { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) @@ -331,6 +309,12 @@ public partial class MainWindowViewModel : ViewModelBase }); Debug.WriteLine($"Database loaded on UI - name: {db.Name}, version: {db.Version}"); } + + private void ShowErrorDialog(string message) + { + var error = new ErrorDialogViewModel(message); + error.ShowDialog(); + } private void TagDatabase_TagAdded(object? sender, Tag _) { diff --git a/UI/ViewModels/SearchViewModel.cs b/UI/ViewModels/SearchViewModel.cs index 7f6e957..e884cda 100644 --- a/UI/ViewModels/SearchViewModel.cs +++ b/UI/ViewModels/SearchViewModel.cs @@ -9,7 +9,7 @@ namespace TagHierarchyManager.UI.ViewModels; public partial class SearchViewModel : ViewModelBase, IDisposable { - private readonly MainWindowViewModel mainWindow; + private readonly MainWindowViewModel _mainWindow; [ObservableProperty] private ObservableCollection _searchResults = []; @@ -17,34 +17,34 @@ public partial class SearchViewModel : ViewModelBase, IDisposable public SearchViewModel(MainWindowViewModel mainWindow) { - this.mainWindow = mainWindow; - mainWindow.Database.TagDeleted += this.TagDatabase_OnTagDeleted; + this._mainWindow = mainWindow; + mainWindow.Database?.TagDeleted += this.TagDatabase_OnTagDeleted; } - public void Dispose() => this.mainWindow.Database?.TagDeleted -= this.TagDatabase_OnTagDeleted; + public void Dispose() => this._mainWindow.Database?.TagDeleted -= this.TagDatabase_OnTagDeleted; public void Search(string searchQuery, TagDatabaseSearchMode mode, bool searchAliases) { - if (this.mainWindow.Database == null || string.IsNullOrWhiteSpace(searchQuery)) return; + if (this._mainWindow.Database == null || string.IsNullOrWhiteSpace(searchQuery)) return; var results = searchAliases - ? this.mainWindow.Database.SearchWithAliases(searchQuery, mode) - : this.mainWindow.Database.Search(searchQuery, mode); + ? this._mainWindow.Database.SearchWithAliases(searchQuery, mode) + : this._mainWindow.Database.Search(searchQuery, mode); this.SearchResults.Clear(); if (results.Count == 0) { - this.mainWindow.StatusBlockText = Resources.SearchNoResultsFound; + this._mainWindow.StatusBlockText = Resources.SearchNoResultsFound; return; } results.Select(tag => - new TagItemViewModel(tag, id => this.mainWindow.Database.Tags.FirstOrDefault(t => t.Id == id)?.Name)) + new TagItemViewModel(tag, id => this._mainWindow.Database.Tags.FirstOrDefault(t => t.Id == id)?.Name)) .OrderBy(tag => tag.Name) .ToList() .ForEach(this.SearchResults.Add); - this.mainWindow.StatusBlockText = results.Count > 1 + this._mainWindow.StatusBlockText = results.Count > 1 ? string.Format(Resources.SearchMultipleResultsFound, results.Count) : Resources.SearchOneResultFound; } @@ -52,7 +52,7 @@ public partial class SearchViewModel : ViewModelBase, IDisposable partial void OnSelectedSearchResultChanged(TagItemViewModel? value) { if (value is null) return; - this.mainWindow.SelectedTag = value; + this._mainWindow.SelectedTag = value; } private void TagDatabase_OnTagDeleted(object? sender, (int id, string name) deletedTag) diff --git a/UI/ViewModels/TagItemViewModel.cs b/UI/ViewModels/TagItemViewModel.cs index 78654dd..1a4cfa7 100644 --- a/UI/ViewModels/TagItemViewModel.cs +++ b/UI/ViewModels/TagItemViewModel.cs @@ -11,17 +11,17 @@ namespace TagHierarchyManager.UI.ViewModels; public partial class TagItemViewModel(Tag tag, Func? getNameById = null) : ViewModelBase { - [ObservableProperty] private string _editingAliases; + [ObservableProperty] private string _editingAliases = string.Empty; [ObservableProperty] private bool _editingIsTopLevel; [ObservableProperty] private string _editingName = tag.Name; - [ObservableProperty] private string _editingNotes; + [ObservableProperty] private string _editingNotes = string.Empty; - [ObservableProperty] private string _editingParents; + [ObservableProperty] private string _editingParents = string.Empty; - [ObservableProperty] private string _editingTagBindings; + [ObservableProperty] private string _editingTagBindings = string.Empty; private bool _isInitialising; diff --git a/UI/Views/Common.cs b/UI/Views/Common.cs index 747d8ed..0aa25e2 100644 --- a/UI/Views/Common.cs +++ b/UI/Views/Common.cs @@ -3,7 +3,7 @@ using TagHierarchyManager.UI.Assets; namespace TagHierarchyManager.UI.Views; -public class Common +public static class Common { public static FilePickerFileType MusicBeeTagHierarchy { get; } = new(Resources.FileFormatMusicBeeTagHierarchy) { diff --git a/UI/Views/ImportDialog.axaml.cs b/UI/Views/ImportDialog.axaml.cs index cd05bff..b2fb31c 100644 --- a/UI/Views/ImportDialog.axaml.cs +++ b/UI/Views/ImportDialog.axaml.cs @@ -11,10 +11,17 @@ public partial class ImportDialog : Window public ImportDialog() { this.InitializeComponent(); - this.DataContextChanged += (s, e) => + + this.DataContextChanged += (_, _) => { if (this.ViewModel != null) this.ViewModel.RequestClose += this.Close; }; + + this.Unloaded += (_, _) => + { + if (this.ViewModel != null) + this.ViewModel.RequestClose -= this.Close; + }; } private ImportDialogViewModel? ViewModel => this.DataContext as ImportDialogViewModel; @@ -68,6 +75,19 @@ public partial class ImportDialog : Window public void ButtonCancel_Click(object? sender, RoutedEventArgs e) => this.Close(); - public async void ButtonImport_Click(object? sender, RoutedEventArgs e) => - await this.ViewModel?.InitiateImport(); + 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 b152251..3db912f 100644 --- a/UI/Views/MainWindow.axaml +++ b/UI/Views/MainWindow.axaml @@ -46,8 +46,8 @@ + ItemsSource="{Binding HierarchyTreeViewModel.TopLevelTags, FallbackValue={x:Null}}" + SelectedItem="{Binding HierarchyTreeViewModel.SelectedTag, Mode=TwoWay, FallbackValue={x:Null}}"> @@ -106,30 +106,30 @@ RowDefinitions="Auto, Auto, Auto, Auto, *, Auto" Margin="5" IsEnabled="{Binding SelectedTag, Converter={x:Static ObjectConverters.IsNotNull}}"> - - - - - - diff --git a/UI/Views/MainWindow.axaml.cs b/UI/Views/MainWindow.axaml.cs index 1ccaefa..d9b7bf7 100644 --- a/UI/Views/MainWindow.axaml.cs +++ b/UI/Views/MainWindow.axaml.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -20,50 +21,90 @@ public partial class MainWindow : Window public void ButtonAdd_Click(object? sender, RoutedEventArgs e) { - this.ViewModel?.NewTag(); + if (this.ViewModel?.SelectedTag is null) return; + this.ViewModel.NewTag(); } public void ButtonCancel_Click(object? sender, RoutedEventArgs e) { - this.ViewModel?.SelectedTag.BeginEdit(); + this.ViewModel?.SelectedTag?.BeginEdit(); } public void ButtonDelete_Click(object? sender, RoutedEventArgs e) { - this.ViewModel?.StartTagDeletion(); + 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) { - await this.ViewModel?.SaveSelectedTagAsync(); + try + { + if (this.ViewModel?.SelectedTag is null) return; + await this.ViewModel.SaveSelectedTagAsync(); + } + catch (Exception ex) + { + var error = new ErrorDialogViewModel(ex.Message); + error.ShowDialog(); + } } public void ButtonSearch_Click(object? sender, RoutedEventArgs e) { - this.ViewModel?.StartSearch( - this.SearchTextBox.Text!, - (TagDatabaseSearchMode)this.SearchModeComboBox.SelectedIndex, - this.SearchAliasesCheckBox.IsChecked ?? false); + try + { + if (this.ViewModel?.Database is null) return; + this.ViewModel?.StartSearch( + this.SearchTextBox.Text!, + (TagDatabaseSearchMode)this.SearchModeComboBox.SelectedIndex, + this.SearchAliasesCheckBox.IsChecked ?? false); + } + catch (Exception ex) + { + var error = new ErrorDialogViewModel(ex.Message); + error.ShowDialog(); + } + } public void MenuItemDatabaseSettings_Click(object? sender, RoutedEventArgs e) { - this.ViewModel?.ShowDatabaseSettings(); + if (this.ViewModel?.Database == null) return; + this.ViewModel.ShowDatabaseSettings(); } + // Resharper disable once AsyncVoidEventHandlerMethod public async void MenuItemExport_Click(object? sender, RoutedEventArgs e) { - var file = await this.StorageProvider.SaveFilePickerAsync( - new FilePickerSaveOptions - { - Title = Assets.Resources.DialogTitleExportTagDatabase, - FileTypeChoices = [Common.MusicBeeTagHierarchy], - SuggestedFileName = this.ViewModel?.Database.Name - }); - if (file == null) return; - var path = file.TryGetLocalPath(); - if (path == null) return; - await this.ViewModel?.ExportAsync(path); + 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(); + } } public void MenuItemImport_Click(object? sender, RoutedEventArgs e) @@ -73,32 +114,48 @@ public partial class MainWindow : Window public async void MenuItemNew_Click(object? sender, RoutedEventArgs e) { - if (this.ViewModel is null) return; - var file = await this.StorageProvider.SaveFilePickerAsync( - new FilePickerSaveOptions - { - Title = Assets.Resources.DialogTitleSaveDatabaseAs, - FileTypeChoices = [Common.TagDatabaseFileType] - }); - if (file == null) return; - var path = file.TryGetLocalPath(); - if (path == null) return; - await this.ViewModel.CreateNewDatabase(path); + 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) { - 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); + 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(); + } } @@ -121,25 +178,38 @@ public partial class MainWindow : Window public async void WindowClosing(object? sender, WindowClosingEventArgs e) { - if (this._userWantsToQuit) return; - if (this.ViewModel.UnsavedChanges) + try { - e.Cancel = true; - var result = await this.ViewModel.ShowNullableBoolDialog(new UnsavedChangesDialog()); - switch (result) + if (this._userWantsToQuit) return; + if (this.ViewModel is not null && this.ViewModel.UnsavedChanges) { - case true: - await this.ViewModel.SaveSelectedTagAsync(); - this._userWantsToQuit = true; - this.Close(); - break; - case false: - this._userWantsToQuit = true; - this.Close(); - break; - case null: - break; + e.Cancel = true; + var result = await this.ViewModel.ShowNullableBoolDialog(new UnsavedChangesDialog()); + switch (result) + { + case true: + await this.ViewModel.SaveSelectedTagAsync(); + this._userWantsToQuit = true; + this.Close(); + break; + case false: + this._userWantsToQuit = true; + this.Close(); + break; + case null: + break; + } } + else + { + this._userWantsToQuit = true; + this.Close(); + } + } + catch (Exception ex) + { + var error = new ErrorDialogViewModel(ex.Message); + error.ShowDialog(); } } } \ No newline at end of file -- 2.51.2