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);
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.Show();
};

View File

@@ -1,59 +1,109 @@
using Adw;
using Keychain.ViewModels;
namespace Keychain.UI;
public class AddShortcutWindow
{
private readonly PasswordStoreShortcutCollection shortcuts;
public Dialog Dialog { get; }
private const string dialogId = "add_shortcut_dialog";
private Gtk.Button? closeButton;
private const string closeButtonId = "close_button";
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 const string browseButtonId = "folder_browse_button";
private Gtk.Button? clearSelectedFolderButton;
private const string clearSelectedFolderButtonId = "clear_selected_folder_button";
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");
Dialog = builder.GetObject("add_shortcut_dialog") as Dialog;
Dialog = builder.GetObject(dialogId) as Dialog;
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;
if (closeButton == null)
try
{
throw new Exception("Failed to load UI element with ID: close_button");
}
closeButton.OnClicked += Close;
iconPickerButton = builder.GetObject("icon_picker_button") as Gtk.Button;
if (iconPickerButton == null)
{
throw new Exception("Failed to load UI element with ID: icon_picker_button");
}
iconPickerButton.OnClicked += OpenIconPicker;
closeButton = builder.GetObject(closeButtonId) as Gtk.Button;
if (closeButton == null)
{
throw new NullReferenceException(closeButtonId);
}
closeButton.OnClicked += Close;
clearSelectedFolderButton = builder.GetObject("clear_selected_folder_button") as Gtk.Button;
if (clearSelectedFolderButton == null)
{
throw new Exception("Failed to load UI element with ID: icon_picker_button");
}
clearSelectedFolderButton.OnClicked += ClearSelectedFolder;
iconPickerButton = builder.GetObject(iconPickerButtonId) as Gtk.Button;
if (iconPickerButton == null)
{
throw new NullReferenceException(iconPickerButtonId);
}
iconPickerButton.OnClicked += OpenIconPicker;
browseButton = builder.GetObject("folder_browse_button") as Gtk.Button;
if (browseButton == null)
{
throw new Exception("Failed to load UI element with ID: folder_browse_button");
}
browseButton.OnClicked += BrowseFolder;
displayNameEntryRow = builder.GetObject(displayNameEntryRowId) as EntryRow;
if (displayNameEntryRow == null)
{
throw new NullReferenceException(displayNameEntryRowId);
}
folderActionRow = builder.GetObject(folderActionRowId) as ActionRow;
if (folderActionRow == null)
{
throw new NullReferenceException(folderActionRowId);
}
saveButton = builder.GetObject("save_button") as Gtk.Button;
if (saveButton == null)
{
throw new Exception("Failed to load UI element with ID: save_button");
clearSelectedFolderButton = builder.GetObject(clearSelectedFolderButtonId) as Gtk.Button;
if (clearSelectedFolderButton == null)
{
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)

View File

@@ -34,7 +34,7 @@
<child>
<object class="AdwPreferencesGroup">
<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>
</object>
</child>
@@ -51,7 +51,7 @@
</object>
</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>
<child>
<object class="GtkBox">

View File

@@ -1,5 +1,6 @@
using Adw;
using Keychain.ViewModels;
using Logic;
namespace Keychain.UI;
@@ -12,6 +13,8 @@ public class MainWindow
private Gtk.Stack titleStack;
private Gtk.SearchEntry searchEntry;
private readonly IPasswordStoreService passwordStoreService;
private readonly string windowId = "main_window";
private readonly string shortcutsGroupId = "shortcuts_group";
private readonly string addShortcutButtonId = "add_shortcut_button";
@@ -19,8 +22,9 @@ public class MainWindow
private readonly string titleStackId = "title_stack";
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");
@@ -74,38 +78,27 @@ public class MainWindow
throw new Exception("Failed to load UI element with ID: " + e.Message);
}
// Initialize the observable collection with property binding
shortcuts = new PasswordStoreShortcutCollection(shortcutsGroup);
// Initialize the observable collection with property binding
shortcuts = new PasswordStoreShortcutCollection(shortcutsGroup, passwordStoreService);
LoadDefaultShortcuts();
}
private void OnAddShortcutClicked(object sender, EventArgs e)
{
var dialog = new AddShortcutWindow().Dialog;
var dialog = new AddShortcutWindow(shortcuts).Dialog;
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()
{
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)
{
shortcut.DisplayName = newName; // This will automatically update the UI row
}
// private void UpdateShortcutName(PasswordStoreViewModel shortcut, string newName)
// {
// shortcut.DisplayName = newName; // This will automatically update the UI row
// }
private void SetSearchBarVisible(object sender, EventArgs e)
{

View File

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