Implemented proper viewModels

This commit is contained in:
2025-09-30 11:31:38 +02:00
parent c8c24cb7cc
commit a99b9fe0bb
7 changed files with 184 additions and 110 deletions

View File

@@ -15,7 +15,8 @@ class Program
var application = Adw.Application.New("org.typomustakes.keychain", Gio.ApplicationFlags.FlagsNone); var application = Adw.Application.New("org.typomustakes.keychain", Gio.ApplicationFlags.FlagsNone);
application.OnActivate += (sender, args) => application.OnActivate += (sender, args) =>
{ {
var window = new UI.MainWindow().Window; var passwordStoreService = provider.GetRequiredService<IPasswordStoreService>();
var window = new UI.MainWindow(passwordStoreService).Window;
window.Application = (Adw.Application)sender; window.Application = (Adw.Application)sender;
window.Show(); window.Show();
}; };

View File

@@ -1,59 +1,109 @@
using Adw; using Adw;
using Keychain.ViewModels;
namespace Keychain.UI; namespace Keychain.UI;
public class AddShortcutWindow public class AddShortcutWindow
{ {
private readonly PasswordStoreShortcutCollection shortcuts;
public Dialog Dialog { get; } public Dialog Dialog { get; }
private const string dialogId = "add_shortcut_dialog";
private Gtk.Button? closeButton; private Gtk.Button? closeButton;
private const string closeButtonId = "close_button";
private Gtk.Button? iconPickerButton; private Gtk.Button? iconPickerButton;
private const string iconPickerButtonId = "icon_picker_button";
private EntryRow? displayNameEntryRow;
private const string displayNameEntryRowId = "display_name_entry_row";
private ActionRow? folderActionRow;
private const string folderActionRowId = "folder_action_row";
private Gtk.Button? browseButton; private Gtk.Button? browseButton;
private const string browseButtonId = "folder_browse_button";
private Gtk.Button? clearSelectedFolderButton; private Gtk.Button? clearSelectedFolderButton;
private const string clearSelectedFolderButtonId = "clear_selected_folder_button";
private Gtk.Button? saveButton; private Gtk.Button? saveButton;
private const string saveButtonId = "save_button";
public AddShortcutWindow() public AddShortcutWindow(PasswordStoreShortcutCollection shortcuts)
{ {
this.shortcuts = shortcuts;
var builder = new Gtk.Builder("Keychain.UI.AddShortcutWindow.AddShortcutWindow.xml"); var builder = new Gtk.Builder("Keychain.UI.AddShortcutWindow.AddShortcutWindow.xml");
Dialog = builder.GetObject("add_shortcut_dialog") as Dialog; Dialog = builder.GetObject(dialogId) as Dialog;
if (Dialog == null) if (Dialog == null)
{ {
throw new Exception("Failed to load embedded resource AddShortcutWindow.xml"); throw new NullReferenceException("Failed to load embedded resource AddShortcutWindow.xml");
} }
closeButton = builder.GetObject("close_button") as Gtk.Button; try
if (closeButton == null)
{ {
throw new Exception("Failed to load UI element with ID: close_button");
}
closeButton.OnClicked += Close;
iconPickerButton = builder.GetObject("icon_picker_button") as Gtk.Button; closeButton = builder.GetObject(closeButtonId) as Gtk.Button;
if (iconPickerButton == null) if (closeButton == null)
{ {
throw new Exception("Failed to load UI element with ID: icon_picker_button"); throw new NullReferenceException(closeButtonId);
} }
iconPickerButton.OnClicked += OpenIconPicker; closeButton.OnClicked += Close;
clearSelectedFolderButton = builder.GetObject("clear_selected_folder_button") as Gtk.Button; iconPickerButton = builder.GetObject(iconPickerButtonId) as Gtk.Button;
if (clearSelectedFolderButton == null) if (iconPickerButton == null)
{ {
throw new Exception("Failed to load UI element with ID: icon_picker_button"); throw new NullReferenceException(iconPickerButtonId);
} }
clearSelectedFolderButton.OnClicked += ClearSelectedFolder; iconPickerButton.OnClicked += OpenIconPicker;
browseButton = builder.GetObject("folder_browse_button") as Gtk.Button; displayNameEntryRow = builder.GetObject(displayNameEntryRowId) as EntryRow;
if (browseButton == null) if (displayNameEntryRow == null)
{ {
throw new Exception("Failed to load UI element with ID: folder_browse_button"); throw new NullReferenceException(displayNameEntryRowId);
} }
browseButton.OnClicked += BrowseFolder;
folderActionRow = builder.GetObject(folderActionRowId) as ActionRow;
if (folderActionRow == null)
{
throw new NullReferenceException(folderActionRowId);
}
saveButton = builder.GetObject("save_button") as Gtk.Button; clearSelectedFolderButton = builder.GetObject(clearSelectedFolderButtonId) as Gtk.Button;
if (saveButton == null) if (clearSelectedFolderButton == null)
{ {
throw new Exception("Failed to load UI element with ID: save_button"); throw new NullReferenceException(clearSelectedFolderButtonId);
}
clearSelectedFolderButton.OnClicked += ClearSelectedFolder;
browseButton = builder.GetObject(browseButtonId) as Gtk.Button;
if (browseButton == null)
{
throw new NullReferenceException(browseButtonId);
}
browseButton.OnClicked += BrowseFolder;
saveButton = builder.GetObject(saveButtonId) as Gtk.Button;
if (saveButton == null)
{
throw new NullReferenceException(saveButtonId);
}
saveButton.OnClicked += (sender, e) => CreateShortcut();
} }
catch (NullReferenceException e)
{
throw new Exception($"Failed to load UI element: {e.Message}");
}
}
private void CreateShortcut()
{
if (displayNameEntryRow == null || browseButton == null || iconPickerButton == null)
return;
var displayName = displayNameEntryRow.GetText();
var path = ((ButtonContent)browseButton.Child).Label;
var iconName = iconPickerButton.Label;
if (string.IsNullOrWhiteSpace(displayName) || path.Equals("Browse") || string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(iconName))
return;
shortcuts.Add(path, displayName, iconName);
Dialog.Close();
} }
private void OpenIconPicker(object sender, EventArgs e) private void OpenIconPicker(object sender, EventArgs e)

View File

@@ -34,7 +34,7 @@
<child> <child>
<object class="AdwPreferencesGroup"> <object class="AdwPreferencesGroup">
<child> <child>
<object class="AdwEntryRow"> <object class="AdwEntryRow" id="display_name_entry_row">
<property name="title" translatable="yes" context="Input field placeholder" comments="Noun. Tells the user that the display name of the new password store is to be supplied here">Name</property> <property name="title" translatable="yes" context="Input field placeholder" comments="Noun. Tells the user that the display name of the new password store is to be supplied here">Name</property>
</object> </object>
</child> </child>
@@ -51,7 +51,7 @@
</object> </object>
</child> </child>
<child> <child>
<object class="AdwActionRow"> <object class="AdwActionRow" id="folder_action_row">
<property name="title" translatable="yes" context="Label" comments="Noun. Marks a button that allows the user to pick a folder where the new store will be.">Location</property> <property name="title" translatable="yes" context="Label" comments="Noun. Marks a button that allows the user to pick a folder where the new store will be.">Location</property>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">

View File

@@ -1,5 +1,6 @@
using Adw; using Adw;
using Keychain.ViewModels; using Keychain.ViewModels;
using Logic;
namespace Keychain.UI; namespace Keychain.UI;
@@ -12,6 +13,8 @@ public class MainWindow
private Gtk.Stack titleStack; private Gtk.Stack titleStack;
private Gtk.SearchEntry searchEntry; private Gtk.SearchEntry searchEntry;
private readonly IPasswordStoreService passwordStoreService;
private readonly string windowId = "main_window"; private readonly string windowId = "main_window";
private readonly string shortcutsGroupId = "shortcuts_group"; private readonly string shortcutsGroupId = "shortcuts_group";
private readonly string addShortcutButtonId = "add_shortcut_button"; private readonly string addShortcutButtonId = "add_shortcut_button";
@@ -19,8 +22,9 @@ public class MainWindow
private readonly string titleStackId = "title_stack"; private readonly string titleStackId = "title_stack";
private readonly string searchEntryId = "search_entry"; private readonly string searchEntryId = "search_entry";
public MainWindow() public MainWindow(IPasswordStoreService passwordStoreService)
{ {
this.passwordStoreService = passwordStoreService;
var builder = new Gtk.Builder("Keychain.UI.MainWindow.MainWindow.xml"); var builder = new Gtk.Builder("Keychain.UI.MainWindow.MainWindow.xml");
@@ -74,38 +78,27 @@ public class MainWindow
throw new Exception("Failed to load UI element with ID: " + e.Message); throw new Exception("Failed to load UI element with ID: " + e.Message);
} }
// Initialize the observable collection with property binding // Initialize the observable collection with property binding
shortcuts = new PasswordStoreShortcutCollection(shortcutsGroup); shortcuts = new PasswordStoreShortcutCollection(shortcutsGroup, passwordStoreService);
LoadDefaultShortcuts(); LoadDefaultShortcuts();
} }
private void OnAddShortcutClicked(object sender, EventArgs e) private void OnAddShortcutClicked(object sender, EventArgs e)
{ {
var dialog = new AddShortcutWindow().Dialog; var dialog = new AddShortcutWindow(shortcuts).Dialog;
dialog.Present(Window); dialog.Present(Window);
} }
private void AddShortcut(string path)
{
var newShortcut = new PasswordStoreShortcut(path: path);
shortcuts.Add(newShortcut); // This will automatically update the UI
}
private void RemoveShortcut(PasswordStoreViewModel shortcut)
{
shortcuts.Remove(shortcut); // This will automatically update the UI
}
private void LoadDefaultShortcuts() private void LoadDefaultShortcuts()
{ {
shortcuts.Add(new PasswordStoreShortcut(displayName: "Default", path: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.password_store")); shortcuts.Add(new PasswordStoreViewModel(displayName: "Default", path: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.password_store"));
} }
private void UpdateShortcutName(PasswordStoreViewModel shortcut, string newName) // private void UpdateShortcutName(PasswordStoreViewModel shortcut, string newName)
{ // {
shortcut.DisplayName = newName; // This will automatically update the UI row // shortcut.DisplayName = newName; // This will automatically update the UI row
} // }
private void SetSearchBarVisible(object sender, EventArgs e) private void SetSearchBarVisible(object sender, EventArgs e)
{ {

View File

@@ -2,68 +2,74 @@ namespace Keychain.ViewModels;
using Adw; using Adw;
using Gtk; using Gtk;
using Logic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
public class PasswordStoreShortcutCollection : ObservableCollection<PasswordStoreViewModel> public class PasswordStoreShortcutCollection : ObservableCollection<PasswordStoreViewModel>
{ {
private readonly PreferencesGroup shortcutsGroup; private IPasswordStoreService _passwordStoreService;
private readonly Dictionary<PasswordStoreViewModel, ActionRow> itemToRowMap = new();
private readonly PreferencesGroup shortcutsGroup;
public PasswordStoreShortcutCollection(PreferencesGroup shortcutsGroup) private readonly Dictionary<PasswordStoreViewModel, ActionRow> itemToRowMap = new();
{
this.shortcutsGroup = shortcutsGroup; public PasswordStoreShortcutCollection(PreferencesGroup shortcutsGroup, IPasswordStoreService passwordStoreService)
CollectionChanged += OnCollectionChanged; : base()
} {
this.shortcutsGroup = shortcutsGroup;
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) _passwordStoreService = passwordStoreService;
{ CollectionChanged += OnCollectionChanged;
switch (e.Action) }
{
case NotifyCollectionChangedAction.Add: private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
foreach (PasswordStoreViewModel item in e.NewItems) {
{ switch (e.Action)
var row = CreateShortcutRow(item); {
itemToRowMap[item] = row; case NotifyCollectionChangedAction.Add:
shortcutsGroup.Add(row); foreach (PasswordStoreViewModel item in e.NewItems)
{
var row = CreateShortcutRow(item);
itemToRowMap[item] = row;
shortcutsGroup.Add(row);
// Subscribe to property changes for reactive updates // Subscribe to property changes for reactive updates
item.PropertyChanged += (sender, args) => UpdateRowFromItem(item, ref row); item.PropertyChanged += (sender, args) => UpdateRowFromItem(item, ref row);
} }
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
foreach (PasswordStoreViewModel item in e.OldItems) foreach (PasswordStoreViewModel item in e.OldItems)
{ {
if (itemToRowMap.TryGetValue(item, out var row)) if (itemToRowMap.TryGetValue(item, out var row))
{ {
shortcutsGroup.Remove(row); shortcutsGroup.Remove(row);
itemToRowMap.Remove(item); itemToRowMap.Remove(item);
} }
} }
break; break;
case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Reset:
foreach (var row in itemToRowMap.Values) foreach (var row in itemToRowMap.Values)
{ {
shortcutsGroup.Remove(row); shortcutsGroup.Remove(row);
} }
itemToRowMap.Clear(); itemToRowMap.Clear();
break; break;
} }
} }
private ActionRow CreateShortcutRow(PasswordStoreViewModel shortcut) private ActionRow CreateShortcutRow(PasswordStoreViewModel shortcut)
{ {
var row = new ActionRow(); var row = new ActionRow();
UpdateRowFromItem(shortcut, ref row); UpdateRowFromItem(shortcut, ref row);
row.SetActivatable(true); row.SetActivatable(true);
row.OnActivated += (sender, args) => { row.OnActivated += (sender, args) =>
Console.WriteLine($"[DEBUG] Opening: {shortcut.Path}"); {
}; Console.WriteLine($"[DEBUG] Opening: {shortcut.Path}");
};
return row;
return row;
} }
private void UpdateRowFromItem(PasswordStoreViewModel shortcut, ref ActionRow row) private void UpdateRowFromItem(PasswordStoreViewModel shortcut, ref ActionRow row)
@@ -91,4 +97,11 @@ public class PasswordStoreShortcutCollection : ObservableCollection<PasswordStor
edit.IconName = "document-edit-symbolic"; edit.IconName = "document-edit-symbolic";
row.AddSuffix(edit); row.AddSuffix(edit);
} }
public void Add(string path, string? displayName = null, string? iconName = null)
{
PasswordStoreViewModel item = new PasswordStoreViewModel(path, displayName, iconName);
Add(item);
_passwordStoreService.Create(item.Model);
}
} }

View File

@@ -23,8 +23,20 @@ public class PasswordStoreViewModel : INotifyPropertyChanged
get => _model.Path; get => _model.Path;
} }
public PasswordStore Model { get => _model; }
public PasswordStoreViewModel(PasswordStore item) public PasswordStoreViewModel(PasswordStore item)
{ {
_model = item; _model = item;
} }
public PasswordStoreViewModel(string path, string? displayName = "New Shortcut", string? iconName = "text-x-generic-symbolic")
{
_model = new PasswordStore
{
DisplayName = displayName,
Path = path,
IconName = iconName
};
}
} }

View File

@@ -7,6 +7,11 @@ public class PasswordStoreService : IPasswordStoreService
{ {
private readonly IRepository repository; private readonly IRepository repository;
public PasswordStoreService(IRepository repository)
{
this.repository = repository;
}
public void Create(PasswordStore item) public void Create(PasswordStore item)
{ {
repository.Create(item); repository.Create(item);