diff --git a/Core/Models/Tag/Tag.cs b/Core/Models/Tag/Tag.cs
index 13f20a1..415ee4d 100644
--- a/Core/Models/Tag/Tag.cs
+++ b/Core/Models/Tag/Tag.cs
@@ -32,9 +32,7 @@ public partial class Tag
/// This is mainly for internal use by the user.
///
public string Notes { get; set; } = string.Empty;
-
- // TODO ensure ParentIds is the authority on parent-child relationships in both the core + UI.
- // TODO convert these to hashsets
+
///
/// Gets or sets a list of the tag entry's parent IDs for interaction with the database.
///
@@ -56,8 +54,8 @@ public partial class Tag
/// Gets or sets the date the tag was created.
/// Defaults to null to cater to migrated databases to prevent inaccurate information.
///
- public DateTime? CreatedAt { get; set; } = null;
-
+ public DateTime? CreatedAt { get; set; }
+
///
/// Gets or sets the date the tag was modified.
///
diff --git a/Tests/TagDatabaseSaveTests.cs b/Tests/TagDatabaseSaveTests.cs
index 2b317aa..f5f2a20 100644
--- a/Tests/TagDatabaseSaveTests.cs
+++ b/Tests/TagDatabaseSaveTests.cs
@@ -1,11 +1,8 @@
-using System.Xml.XPath;
using NUnit.Framework;
using TagHierarchyManager.Models;
namespace TagHierarchyManager.Tests;
-// todo catch events here.
-
///
/// Tests relating to saving of objects to a .
///
diff --git a/UI/Assets/Resources.resx b/UI/Assets/Resources.resx
index fb41806..90e395c 100644
--- a/UI/Assets/Resources.resx
+++ b/UI/Assets/Resources.resx
@@ -208,7 +208,7 @@
No file picked.
-
+
Import...
@@ -265,4 +265,28 @@
{0} (on row {1})
+
+ Date added
+
+
+ Date modified
+
+
+ Recently Added
+
+
+ Recently Edited
+
+
+ NOTE: Only newly added tags will show up on this table. If you have recently migrated, you will not see any tags here.
+
+
+ Recently added/edited tags
+
+
+ Recently Added/Edited Tags
+
+
+ Bulk Add Tags...
+
\ No newline at end of file
diff --git a/UI/TagDatabaseService.cs b/UI/TagDatabaseService.cs
index 895ca2d..6899078 100644
--- a/UI/TagDatabaseService.cs
+++ b/UI/TagDatabaseService.cs
@@ -29,12 +29,6 @@ public class TagDatabaseService : ObservableObject
public int TagCount => this.Database?.Tags.Count ?? 0;
public int TagRelationshipCount => this.Database?.GetTagRelationshipCount() ?? 0;
- public sealed record TagWriteResult(
- IReadOnlyList Added,
- IReadOnlyList Updated,
- IReadOnlyList<(int id, string name)> Deleted
- );
-
private TagDatabase? Database { get; set; }
public void CloseDatabase()
@@ -113,6 +107,20 @@ public class TagDatabaseService : ObservableObject
.ToList();
}
+ public List GetRecentTags(bool recentlyAdded)
+ {
+ if (this.Database is null) return [];
+ return recentlyAdded
+ ? this.Database.Tags.Where(t => t.CreatedAt is not null).OrderByDescending(t => t.CreatedAt).Take(50)
+ .ToList()
+ : this.Database.Tags.OrderByDescending(t => t.UpdatedAt).Take(50).ToList();
+ }
+
+ public Tag? GetTagById(int id)
+ {
+ return this.Database?.Tags.FirstOrDefault(t => t.Id == id);
+ }
+
public async Task LoadDatabase(string filePath)
{
TagDatabase db = new();
@@ -120,7 +128,7 @@ public class TagDatabaseService : ObservableObject
{
if (this.Database != null)
this.CloseDatabase();
-
+
db.InitialisationComplete += this.TagDatabase_OnInitialisationComplete;
await Task.Run(() => db.LoadAsync(filePath));
}
@@ -145,7 +153,7 @@ public class TagDatabaseService : ObservableObject
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
this.NotifyDatabasePropertiesChanged();
}
-
+
public async Task WriteTagsToDatabase(List tags)
{
if (this.Database is null) return;
@@ -193,7 +201,7 @@ public class TagDatabaseService : ObservableObject
if (this.Database is null) return;
this.Database.TagsWritten += this.TagDatabase_OnTagsWritten;
}
-
+
private void TagDatabase_OnInitialisationComplete(object? sender, EventArgs e)
{
this.Database = sender as TagDatabase;
@@ -214,4 +222,10 @@ public class TagDatabaseService : ObservableObject
if (this.Database is null) return;
this.Database.TagsWritten -= this.TagDatabase_OnTagsWritten;
}
+
+ public sealed record TagWriteResult(
+ IReadOnlyList Added,
+ IReadOnlyList Updated,
+ IReadOnlyList<(int id, string name)> Deleted
+ );
}
\ No newline at end of file
diff --git a/UI/ViewModels/MainWindowViewModel.cs b/UI/ViewModels/MainWindowViewModel.cs
index 7e4c523..b4b518f 100644
--- a/UI/ViewModels/MainWindowViewModel.cs
+++ b/UI/ViewModels/MainWindowViewModel.cs
@@ -22,7 +22,6 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty] private bool _isDbEnabled;
-
private bool _isSwitching;
[ObservableProperty] private SearchViewModel? _searchViewModel;
@@ -44,9 +43,7 @@ public partial class MainWindowViewModel : ViewModelBase
this.TagDatabaseService.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(this.TagDatabaseService.TagCount))
- {
this.OnPropertyChanged(nameof(this.TotalTags));
- }
};
}
@@ -81,94 +78,6 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
- [RelayCommand(AllowConcurrentExecutions = false)]
- private async Task NewTag()
- {
- var userWantsToSave = await this.ShowUnsavedChangesDialog();
- if (userWantsToSave is null) return;
-
- this.SelectedTag = new TagItemViewModel(
- new Tag
- {
- Name = string.Empty,
- IsTopLevel = true,
- TagBindings = this.TagDatabaseService.DefaultTagBindings
- }
- );
- this.UnsavedChanges = true;
- }
-
- [RelayCommand]
- private async Task SaveSelectedTagAsync()
- {
- try
- {
- if (this.SelectedTag is null || !this.TagDatabaseService.IsDatabaseOpen || !this.IsDbEnabled) return;
-
- this.SelectedTag.CommitEdit();
- await this.TagDatabaseService.WriteTagsToDatabase([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);
- }
- }
-
- [RelayCommand]
- private void ShowBulkAddDialog()
- {
- if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
- return;
-
- var dialog = new BulkAddWindow
- {
- DataContext = new BulkAddViewModel(this)
- };
-
- dialog.ShowDialog(desktop.MainWindow!);
- }
-
- [RelayCommand]
- private void ShowDatabaseSettings()
- {
- if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
- return;
-
- var dialog = new DatabaseSettingsWindow
- {
- DataContext = new DatabaseSettingsViewModel(this)
- };
-
- dialog.ShowDialog(desktop.MainWindow!);
- }
-
- [RelayCommand]
- private void ShowImportDialog()
- {
- if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
- return;
-
- var dialog = new ImportDialog
- {
- DataContext = new ImportDialogViewModel(this)
- };
-
- dialog.ShowDialog(desktop.MainWindow!);
- }
-
- private async Task ShowNullableBoolDialog(Window dialog)
- {
- if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
- return null;
-
- var mainWindow = desktop.MainWindow;
- var result = await dialog.ShowDialog(mainWindow!);
- return result;
- }
-
public async Task ShowUnsavedChangesDialog()
{
if (!this.UnsavedChanges) return true;
@@ -176,26 +85,6 @@ public partial class MainWindowViewModel : ViewModelBase
return result;
}
- [RelayCommand]
- private void StartSearch(object? parameter)
- {
- if (parameter is not object[] values || values.Length < 3) return;
-
- var query = values[0] as string ?? string.Empty;
- if (string.IsNullOrWhiteSpace(query)) return;
- var mode = (TagDatabaseSearchMode)values[1];
- var searchAliases = (bool)values[2];
-
- try
- {
- this.SearchViewModel?.Search(query, mode, searchAliases);
- }
- catch (Exception ex)
- {
- this.ShowErrorDialog(ex.Message);
- }
- }
-
[RelayCommand]
private async Task CancelTagEditAsync()
{
@@ -306,6 +195,23 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
+ [RelayCommand(AllowConcurrentExecutions = false)]
+ private async Task NewTag()
+ {
+ var userWantsToSave = await this.ShowUnsavedChangesDialog();
+ if (userWantsToSave is null) return;
+
+ this.SelectedTag = new TagItemViewModel(
+ new Tag
+ {
+ Name = string.Empty,
+ IsTopLevel = true,
+ TagBindings = this.TagDatabaseService.DefaultTagBindings
+ }
+ );
+ this.UnsavedChanges = true;
+ }
+
private void OnUserEditedTag(object? sender, EventArgs e)
{
this.UnsavedChanges = true;
@@ -344,12 +250,97 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
+ [RelayCommand]
+ private async Task SaveSelectedTagAsync()
+ {
+ try
+ {
+ if (this.SelectedTag is null || !this.TagDatabaseService.IsDatabaseOpen || !this.IsDbEnabled) return;
+
+ this.SelectedTag.CommitEdit();
+ await this.TagDatabaseService.WriteTagsToDatabase([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);
+ }
+ }
+
+ [RelayCommand]
+ private void ShowBulkAddDialog()
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
+ return;
+
+ var dialog = new BulkAddWindow
+ {
+ DataContext = new BulkAddViewModel(this)
+ };
+
+ dialog.ShowDialog(desktop.MainWindow!);
+ }
+
+ [RelayCommand]
+ private void ShowDatabaseSettings()
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
+ return;
+
+ var dialog = new DatabaseSettingsWindow
+ {
+ DataContext = new DatabaseSettingsViewModel(this)
+ };
+
+ dialog.ShowDialog(desktop.MainWindow!);
+ }
+
private void ShowErrorDialog(string message)
{
var error = new ErrorDialogViewModel(message);
error.ShowDialog();
}
+ [RelayCommand]
+ private void ShowImportDialog()
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
+ return;
+
+ var dialog = new ImportDialog
+ {
+ DataContext = new ImportDialogViewModel(this)
+ };
+
+ dialog.ShowDialog(desktop.MainWindow!);
+ }
+
+ private async Task ShowNullableBoolDialog(Window dialog)
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
+ return null;
+
+ var mainWindow = desktop.MainWindow;
+ var result = await dialog.ShowDialog(mainWindow!);
+ return result;
+ }
+
+ [RelayCommand]
+ private void ShowRecentsWindow()
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
+ return;
+
+ var dialog = new RecentsWindow
+ {
+ DataContext = new RecentsViewModel(this)
+ };
+
+ dialog.ShowDialog(desktop.MainWindow!);
+ }
+
[RelayCommand]
private async Task StartExportAsync()
{
@@ -388,6 +379,26 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
+ [RelayCommand]
+ private void StartSearch(object? parameter)
+ {
+ if (parameter is not object[] values || values.Length < 3) return;
+
+ var query = values[0] as string ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(query)) return;
+ var mode = (TagDatabaseSearchMode)values[1];
+ var searchAliases = (bool)values[2];
+
+ try
+ {
+ this.SearchViewModel?.Search(query, mode, searchAliases);
+ }
+ catch (Exception ex)
+ {
+ this.ShowErrorDialog(ex.Message);
+ }
+ }
+
[RelayCommand]
private async Task StartTagDeletionAsync()
{
@@ -443,7 +454,7 @@ public partial class MainWindowViewModel : ViewModelBase
this.SelectedTag?.SyncId();
this.OnPropertyChanged(nameof(this.TotalTags));
}
-
+
if (result.Deleted.Count > 0) this.OnPropertyChanged(nameof(this.TotalTags));
}
}
\ No newline at end of file
diff --git a/UI/ViewModels/RecentsViewModel.cs b/UI/ViewModels/RecentsViewModel.cs
new file mode 100644
index 0000000..5965b47
--- /dev/null
+++ b/UI/ViewModels/RecentsViewModel.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using TagHierarchyManager.Models;
+
+namespace TagHierarchyManager.UI.ViewModels;
+
+public partial class RecentsViewModel : ViewModelBase
+{
+ private readonly Func, List> _getParentNamesById;
+ private readonly MainWindowViewModel _mainWindow;
+ [ObservableProperty] private ObservableCollection _recentAdded = [];
+ [ObservableProperty] private ObservableCollection _recentEdited = [];
+
+ public RecentsViewModel(MainWindowViewModel mainWindow)
+ {
+ this._mainWindow = mainWindow;
+ this._getParentNamesById = mainWindow.TagDatabaseService.GetParentNamesByIds;
+ this.RecentAdded = new ObservableCollection();
+ foreach (var tag in this._mainWindow.TagDatabaseService.GetRecentTags(true))
+ if (tag.CreatedAt is not null)
+ this.RecentAdded.Add(new TagRow(tag, tag.CreatedAt.Value));
+
+ foreach (var tag in this._mainWindow.TagDatabaseService.GetRecentTags(false))
+ this.RecentEdited.Add(new TagRow(tag, tag.UpdatedAt));
+ }
+
+ public event Action? RequestClose;
+
+ public void ActivateRowToTagItem(TagRow tagRow)
+ {
+ var tag = this._mainWindow.TagDatabaseService.GetTagById(tagRow.Id);
+ if (tag is null) return;
+ var vm = new TagItemViewModel(tag, this._getParentNamesById);
+ this._mainWindow.SelectedTag = vm;
+ this.RequestClose?.Invoke();
+ }
+
+ public class TagRow(Tag tag, DateTime dateTime)
+ {
+ public DateTime DateTime => dateTime;
+ public int Id => tag.Id;
+ public string Name => tag.Name;
+ }
+}
\ No newline at end of file
diff --git a/UI/Views/BulkAddWindow.axaml b/UI/Views/BulkAddWindow.axaml
index ba2e172..d1f227c 100644
--- a/UI/Views/BulkAddWindow.axaml
+++ b/UI/Views/BulkAddWindow.axaml
@@ -8,7 +8,7 @@
mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="450"
Width="1100" Height="450"
x:Class="TagHierarchyManager.UI.Views.BulkAddWindow"
- Title="{Binding WindowTitle}"
+ Title="{x:Static assets:Resources.TitleBulkAdd}"
KeyDown="BulkAddWindow_OnKeyDown">
-
+ VerticalAlignment="Center" />
+
+ Classes="TextArea ClearButton" />
-
+
diff --git a/UI/Views/ImportDialog.axaml b/UI/Views/ImportDialog.axaml
index 48a0e44..51b6df9 100644
--- a/UI/Views/ImportDialog.axaml
+++ b/UI/Views/ImportDialog.axaml
@@ -8,7 +8,7 @@
Width="600" Height="200" CanResize="False" ShowInTaskbar="False"
x:Class="TagHierarchyManager.UI.Views.ImportDialog"
x:DataType="vm:ImportDialogViewModel"
- Title="{x:Static assets:Resources.ImportDialogTitle}">
+ Title="{x:Static assets:Resources.TitlemportDialog}">
diff --git a/UI/Views/MainWindow.axaml b/UI/Views/MainWindow.axaml
index 430a067..ce3f5be 100644
--- a/UI/Views/MainWindow.axaml
+++ b/UI/Views/MainWindow.axaml
@@ -19,7 +19,7 @@
+ Command="{Binding ShowImportDialogCommand}" />
@@ -32,6 +32,8 @@
Header="{x:Static assets:Resources.MenuDatabase}">
+
-
+
-
-
-
+
+
+
@@ -175,7 +177,7 @@
-
+
diff --git a/UI/Views/MainWindow.axaml.cs b/UI/Views/MainWindow.axaml.cs
index 438c5e3..2f97846 100644
--- a/UI/Views/MainWindow.axaml.cs
+++ b/UI/Views/MainWindow.axaml.cs
@@ -6,10 +6,6 @@ using TagHierarchyManager.UI.ViewModels;
namespace TagHierarchyManager.UI.Views;
-/*
- * TODO:
- * add "recently added" functionality.
- */
public partial class MainWindow : Window
{
private bool _userWantsToQuit;
@@ -20,28 +16,26 @@ public partial class MainWindow : Window
}
private MainWindowViewModel? ViewModel => this.DataContext as MainWindowViewModel;
-
- public void MenuItemQuit_Click(object? sender, RoutedEventArgs e)
- {
- this.Close();
- }
-
+
public void MenuItemAbout_Click(object? sender, RoutedEventArgs e)
{
new AboutWindow().ShowDialog(this);
}
+ public void MenuItemQuit_Click(object? sender, RoutedEventArgs e) =>
+ this.Close();
+
public void SearchTextBox_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
-
- var args = new object[]
- {
- this.SearchTextBox.Text ?? "",
- this.SearchModeComboBox.SelectedIndex,
- this.SearchAliasesCheckBox.IsChecked ?? false
+
+ var args = new object[]
+ {
+ this.SearchTextBox.Text ?? "",
+ this.SearchModeComboBox.SelectedIndex,
+ this.SearchAliasesCheckBox.IsChecked ?? false
};
-
+
if (this.ViewModel?.StartSearchCommand.CanExecute(args) != true) return;
this.ViewModel.StartSearchCommand.Execute(args);
e.Handled = true;
@@ -53,7 +47,6 @@ public partial class MainWindow : Window
{
if (this._userWantsToQuit) return;
-
if (this.ViewModel is not null && this.ViewModel.UnsavedChanges)
{
e.Cancel = true;
diff --git a/UI/Views/RecentsWindow.axaml b/UI/Views/RecentsWindow.axaml
new file mode 100644
index 0000000..2aea177
--- /dev/null
+++ b/UI/Views/RecentsWindow.axaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/UI/Views/RecentsWindow.axaml.cs b/UI/Views/RecentsWindow.axaml.cs
new file mode 100644
index 0000000..7a57148
--- /dev/null
+++ b/UI/Views/RecentsWindow.axaml.cs
@@ -0,0 +1,32 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using TagHierarchyManager.UI.ViewModels;
+
+namespace TagHierarchyManager.UI.Views;
+
+public partial class RecentsWindow : Window
+{
+ public RecentsWindow()
+ {
+ this.InitializeComponent();
+
+ this.DataContextChanged += (_, _) =>
+ {
+ if (this.ViewModel != null) this.ViewModel.RequestClose += this.Close;
+ };
+
+ this.Unloaded += (_, _) =>
+ {
+ if (this.ViewModel != null)
+ this.ViewModel.RequestClose -= this.Close;
+ };
+ }
+
+ private RecentsViewModel? ViewModel => this.DataContext as RecentsViewModel;
+
+ public void DataGrid_DoubleTapped(object sender, RoutedEventArgs e)
+ {
+ if (e.Source is Control control && control.DataContext is RecentsViewModel.TagRow row)
+ this.ViewModel.ActivateRowToTagItem(row);
+ }
+}
\ No newline at end of file