Moved source to subfolder
This commit is contained in:
25
src/App/Keychain.csproj
Normal file
25
src/App/Keychain.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GirCore.Adw-1" Version="0.6.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="UI/MainWindow/MainWindow.xml" />
|
||||
<EmbeddedResource Include="UI/AddShortcutWindow/AddShortcutWindow.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Logic/Logic.csproj" />
|
||||
<ProjectReference Include="../Repository/Repository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
33
src/App/Program.cs
Normal file
33
src/App/Program.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Logic;
|
||||
using Repository;
|
||||
|
||||
namespace Keychain;
|
||||
|
||||
class Program
|
||||
{
|
||||
private static ServiceProvider? provider;
|
||||
|
||||
static int Main()
|
||||
{
|
||||
provider = SetupServices();
|
||||
|
||||
var application = Adw.Application.New("org.typomustakes.keychain", Gio.ApplicationFlags.FlagsNone);
|
||||
application.OnActivate += (sender, args) =>
|
||||
{
|
||||
var window = new UI.MainWindow().Window;
|
||||
window.Application = (Adw.Application)sender;
|
||||
window.Show();
|
||||
};
|
||||
|
||||
return application.RunWithSynchronizationContext(null);
|
||||
}
|
||||
|
||||
private static ServiceProvider SetupServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IPasswordStoreService, PasswordStoreService>();
|
||||
services.AddSingleton<IRepository, JsonRepository>();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
93
src/App/UI/AddShortcutWindow/AddShortcutWindow.cs
Normal file
93
src/App/UI/AddShortcutWindow/AddShortcutWindow.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Adw;
|
||||
|
||||
namespace Keychain.UI;
|
||||
|
||||
public class AddShortcutWindow
|
||||
{
|
||||
public Dialog Dialog { get; }
|
||||
private Gtk.Button? closeButton;
|
||||
private Gtk.Button? iconPickerButton;
|
||||
private Gtk.Button? browseButton;
|
||||
private Gtk.Button? clearSelectedFolderButton;
|
||||
private Gtk.Button? saveButton;
|
||||
|
||||
public AddShortcutWindow()
|
||||
{
|
||||
var builder = new Gtk.Builder("Keychain.UI.AddShortcutWindow.AddShortcutWindow.xml");
|
||||
|
||||
Dialog = builder.GetObject("add_shortcut_dialog") as Dialog;
|
||||
if (Dialog == null)
|
||||
{
|
||||
throw new Exception("Failed to load embedded resource AddShortcutWindow.xml");
|
||||
}
|
||||
|
||||
closeButton = builder.GetObject("close_button") as Gtk.Button;
|
||||
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;
|
||||
if (iconPickerButton == null)
|
||||
{
|
||||
throw new Exception("Failed to load UI element with ID: icon_picker_button");
|
||||
}
|
||||
iconPickerButton.OnClicked += OpenIconPicker;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
saveButton = builder.GetObject("save_button") as Gtk.Button;
|
||||
if (saveButton == null)
|
||||
{
|
||||
throw new Exception("Failed to load UI element with ID: save_button");
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenIconPicker(object sender, EventArgs e)
|
||||
{
|
||||
var chooser = new Gtk.EmojiChooser();
|
||||
chooser.SetParent(iconPickerButton);
|
||||
chooser.OnEmojiPicked += (s, e) =>
|
||||
{
|
||||
iconPickerButton.Label = e.Text;
|
||||
};
|
||||
chooser.Show();
|
||||
}
|
||||
|
||||
private void Close(object sender, EventArgs e)
|
||||
{
|
||||
Dialog.Close();
|
||||
}
|
||||
|
||||
private void ClearSelectedFolder(object sender, EventArgs e)
|
||||
{
|
||||
var buttonContent = (ButtonContent)browseButton.Child;
|
||||
buttonContent.Label = "Browse";
|
||||
clearSelectedFolderButton.Sensitive = false;
|
||||
}
|
||||
|
||||
private async void BrowseFolder(object sender, EventArgs e)
|
||||
{
|
||||
var fileDialog = new Gtk.FileDialog();
|
||||
var selectedFolder = await fileDialog.SelectFolderAsync((Window)Dialog.Parent.Parent);
|
||||
if (selectedFolder != null)
|
||||
{
|
||||
var buttonContent = (ButtonContent)browseButton.Child;
|
||||
buttonContent.Label = selectedFolder.GetPath();
|
||||
clearSelectedFolderButton.Sensitive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/App/UI/AddShortcutWindow/AddShortcutWindow.xml
Normal file
97
src/App/UI/AddShortcutWindow/AddShortcutWindow.xml
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<object class="AdwDialog" id="add_shortcut_dialog">
|
||||
<property name="content-width">400</property>
|
||||
<property name="child">
|
||||
<object class="AdwToolbarView">
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="title-widget">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title" translatable="yes" context="label" comments="Verb. Marks a preferences page where the user can add new password stores. Store as in storage">Add New Store</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="show-end-title-buttons">False</property>
|
||||
<child type="start">
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="valign">center</property>
|
||||
<property name="label" translatable="yes" context="label" comments="Verb">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="save_button">
|
||||
<style>
|
||||
<class name="suggested-action" />
|
||||
</style>
|
||||
<property name="valign">center</property>
|
||||
<property name="label" translatable="yes" context="label" comments="Verb">Save</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<property name="content">
|
||||
<object class="AdwPreferencesPage">
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<child>
|
||||
<object class="AdwEntryRow">
|
||||
<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>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">Icon</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="icon_picker_button">
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="icon-name">emoji-symbols-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<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">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="folder_browse_button">
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="can-shrink">True</property>
|
||||
<property name="icon-name">document-open-symbolic</property>
|
||||
<property name="label">Browse</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_selected_folder_button">
|
||||
<property name="valign">center</property>
|
||||
<property name="sensitive">0</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
122
src/App/UI/MainWindow/MainWindow.cs
Normal file
122
src/App/UI/MainWindow/MainWindow.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Adw;
|
||||
using Keychain.ViewModels;
|
||||
|
||||
namespace Keychain.UI;
|
||||
|
||||
public class MainWindow
|
||||
{
|
||||
public Window Window { get; }
|
||||
private PreferencesGroup shortcutsGroup;
|
||||
private PasswordStoreShortcutCollection shortcuts;
|
||||
private Gtk.ToggleButton searchToggleButton;
|
||||
private Gtk.Stack titleStack;
|
||||
private Gtk.SearchEntry searchEntry;
|
||||
|
||||
private readonly string windowId = "main_window";
|
||||
private readonly string shortcutsGroupId = "shortcuts_group";
|
||||
private readonly string addShortcutButtonId = "add_shortcut_button";
|
||||
private readonly string searchToggleButtonId = "search_button";
|
||||
private readonly string titleStackId = "title_stack";
|
||||
private readonly string searchEntryId = "search_entry";
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
var builder = new Gtk.Builder("Keychain.UI.MainWindow.MainWindow.xml");
|
||||
|
||||
|
||||
Window = builder.GetObject(windowId) as Window;
|
||||
if (Window == null)
|
||||
{
|
||||
throw new Exception("Failed to load embedded resource MainWindow.xml");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
shortcutsGroup = builder.GetObject(shortcutsGroupId) as PreferencesGroup;
|
||||
if (shortcutsGroup == null)
|
||||
throw new Exception(shortcutsGroupId);
|
||||
|
||||
var addButton = builder.GetObject(addShortcutButtonId) as Gtk.Button;
|
||||
if (addButton == null)
|
||||
{
|
||||
throw new Exception(addShortcutButtonId);
|
||||
}
|
||||
addButton.OnClicked += OnAddShortcutClicked;
|
||||
|
||||
searchToggleButton = builder.GetObject(searchToggleButtonId) as Gtk.ToggleButton;
|
||||
if (searchToggleButton == null)
|
||||
{
|
||||
throw new Exception(searchToggleButtonId);
|
||||
}
|
||||
searchToggleButton.OnToggled += SetSearchBarVisible;
|
||||
|
||||
|
||||
titleStack = builder.GetObject(titleStackId) as Gtk.Stack;
|
||||
if (titleStack == null)
|
||||
{
|
||||
throw new Exception(titleStackId);
|
||||
}
|
||||
|
||||
searchEntry = builder.GetObject(searchEntryId) as Gtk.SearchEntry;
|
||||
if (searchEntry == null)
|
||||
{
|
||||
throw new Exception(searchEntryId);
|
||||
}
|
||||
var focusController = new Gtk.EventControllerFocus();
|
||||
focusController.OnLeave += (s, e) =>
|
||||
{
|
||||
searchToggleButton.Active = false;
|
||||
};
|
||||
searchEntry.AddController(focusController);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception("Failed to load UI element with ID: " + e.Message);
|
||||
}
|
||||
|
||||
// Initialize the observable collection with property binding
|
||||
shortcuts = new PasswordStoreShortcutCollection(shortcutsGroup);
|
||||
|
||||
LoadDefaultShortcuts();
|
||||
}
|
||||
|
||||
private void OnAddShortcutClicked(object sender, EventArgs e)
|
||||
{
|
||||
var dialog = new AddShortcutWindow().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"));
|
||||
}
|
||||
|
||||
private void UpdateShortcutName(PasswordStoreViewModel shortcut, string newName)
|
||||
{
|
||||
shortcut.DisplayName = newName; // This will automatically update the UI row
|
||||
}
|
||||
|
||||
private void SetSearchBarVisible(object sender, EventArgs e)
|
||||
{
|
||||
if (searchToggleButton.Active)
|
||||
{
|
||||
titleStack.SetVisibleChildName("Search");
|
||||
searchEntry.GrabFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
titleStack.SetVisibleChildName("Passwords");
|
||||
}
|
||||
}
|
||||
}
|
||||
160
src/App/UI/MainWindow/MainWindow.xml
Normal file
160
src/App/UI/MainWindow/MainWindow.xml
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<object class="AdwWindow" id="main_window">
|
||||
<property name="width-request">350</property>
|
||||
<property name="height-request">300</property>
|
||||
<property name="default-width">800</property>
|
||||
<property name="default-height">500</property>
|
||||
<child>
|
||||
<object class="AdwBreakpoint">
|
||||
<condition>max-width: 500sp</condition>
|
||||
<setter object="split_view" property="collapsed">True</setter>
|
||||
<setter object="show_sidebar_button" property="visible">True</setter>
|
||||
</object>
|
||||
</child>
|
||||
<property name="content">
|
||||
<object class="AdwOverlaySplitView" id="split_view">
|
||||
<property name="min-sidebar-width">300</property>
|
||||
<property name="max-sidebar-width">300</property>
|
||||
<property name="show-sidebar"
|
||||
bind-source="show_sidebar_button"
|
||||
bind-property="active"
|
||||
bind-flags="sync-create|bidirectional"/>
|
||||
<property name="sidebar">
|
||||
<object class="AdwNavigationPage">
|
||||
<property name="title" translatable="yes" context="label" comments="Noun. Marks a list of password collections.">Stores</property>
|
||||
<property name="child">
|
||||
<object class="AdwToolbarView">
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar">
|
||||
<child type="start">
|
||||
<object class="GtkButton" id="add_shortcut_button">
|
||||
<property name="valign">center</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<property name="content">
|
||||
<object class="AdwPreferencesPage">
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup" id="shortcuts_group">
|
||||
<!-- Dynamic rows will be added here via model binding -->
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="content">
|
||||
<object class="AdwNavigationPage">
|
||||
<property name="title" translatable="yes" context="label" comments="Noun, plural. Indicates the location of the actual decryptable passwords">Passwords</property>
|
||||
<property name="child">
|
||||
<object class="AdwToolbarView">
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar">
|
||||
<child type="start">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="search_button">
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="show_sidebar_button">
|
||||
<property name="icon-name">sidebar-show-symbolic</property>
|
||||
<property name="active">True</property>
|
||||
<property name="visible">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<property name="title-widget">
|
||||
<object class="GtkStack" id="title_stack">
|
||||
<property name="transition-type">slide-up-down</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">Passwords</property>
|
||||
<property name="child">
|
||||
<object class="AdwWindowTitle">
|
||||
<property name="title">Passwords</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">Search</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="tightening-threshold">300</property>
|
||||
<property name="maximum-size">400</property>
|
||||
<property name="child">
|
||||
<object class="GtkSearchEntry" id="search_entry">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="placeholder-text" translatable="yes">Search passwords</property>
|
||||
<!-- <signal name="search-started" handler="search_started_cb" swapped="yes"/> -->
|
||||
<!-- <signal name="search-changed" handler="search_changed_cb" swapped="yes"/> -->
|
||||
<!-- <signal name="stop-search" handler="stop_search_cb" swapped="yes"/> -->
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<property name="content">
|
||||
<object class="AdwPreferencesPage">
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title">Default</property>
|
||||
<property name="description">/home/typo/.password-store</property>
|
||||
<property name="header-suffix">
|
||||
<object class="GtkButton">
|
||||
<property name="valign">center</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">Sample password</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">Sample password 2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
94
src/App/ViewModels/PasswordStoreShortcutCollection.cs
Normal file
94
src/App/ViewModels/PasswordStoreShortcutCollection.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
namespace Keychain.ViewModels;
|
||||
|
||||
using Adw;
|
||||
using Gtk;
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
row.SetActivatable(true);
|
||||
row.OnActivated += (sender, args) => {
|
||||
Console.WriteLine($"[DEBUG] Opening: {shortcut.Path}");
|
||||
};
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void UpdateRowFromItem(PasswordStoreViewModel shortcut, ref ActionRow row)
|
||||
{
|
||||
row.SetTitle(shortcut.DisplayName);
|
||||
row.SetSubtitle(shortcut.Path);
|
||||
|
||||
//Update icon
|
||||
var existingIcon = row.GetFirstChild() as Gtk.Image;
|
||||
if (existingIcon == null)
|
||||
{
|
||||
var icon = new Gtk.Image();
|
||||
icon.SetFromIconName(shortcut.IconName);
|
||||
row.AddPrefix(icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingIcon.SetFromIconName(shortcut.IconName);
|
||||
}
|
||||
|
||||
// Edit button
|
||||
var edit = new Button();
|
||||
edit.AddCssClass("flat");
|
||||
edit.SetValign(Align.Center);
|
||||
edit.IconName = "document-edit-symbolic";
|
||||
row.AddSuffix(edit);
|
||||
}
|
||||
}
|
||||
30
src/App/ViewModels/PasswordStoreViewModel.cs
Normal file
30
src/App/ViewModels/PasswordStoreViewModel.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel;
|
||||
using Models;
|
||||
|
||||
namespace Keychain.ViewModels;
|
||||
|
||||
public class PasswordStoreViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private PasswordStore _model;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public string? DisplayName
|
||||
{
|
||||
get => _model.DisplayName;
|
||||
}
|
||||
|
||||
public string? IconName
|
||||
{
|
||||
get => _model.IconName;
|
||||
}
|
||||
|
||||
public string Path
|
||||
{
|
||||
get => _model.Path;
|
||||
}
|
||||
|
||||
public PasswordStoreViewModel(PasswordStore item)
|
||||
{
|
||||
_model = item;
|
||||
}
|
||||
}
|
||||
40
src/Keychain.sln
Normal file
40
src/Keychain.sln
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Models", "Models\Models.csproj", "{FAB7F880-FF3D-496B-B1C6-8356BEC56CE3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logic", "Logic\Logic.csproj", "{88F17494-8F7F-4BA5-96C3-13AF462931A9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keychain", "App\Keychain.csproj", "{D4755A56-58E0-46A3-859B-49ACAA3EDAED}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "Repository\Repository.csproj", "{AB25C193-3B52-46B4-BE6A-0B2E331BD2A8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FAB7F880-FF3D-496B-B1C6-8356BEC56CE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FAB7F880-FF3D-496B-B1C6-8356BEC56CE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FAB7F880-FF3D-496B-B1C6-8356BEC56CE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FAB7F880-FF3D-496B-B1C6-8356BEC56CE3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{88F17494-8F7F-4BA5-96C3-13AF462931A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{88F17494-8F7F-4BA5-96C3-13AF462931A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{88F17494-8F7F-4BA5-96C3-13AF462931A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{88F17494-8F7F-4BA5-96C3-13AF462931A9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4755A56-58E0-46A3-859B-49ACAA3EDAED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D4755A56-58E0-46A3-859B-49ACAA3EDAED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4755A56-58E0-46A3-859B-49ACAA3EDAED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4755A56-58E0-46A3-859B-49ACAA3EDAED}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AB25C193-3B52-46B4-BE6A-0B2E331BD2A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AB25C193-3B52-46B4-BE6A-0B2E331BD2A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AB25C193-3B52-46B4-BE6A-0B2E331BD2A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AB25C193-3B52-46B4-BE6A-0B2E331BD2A8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
13
src/Logic/IPasswordStoreService.cs
Normal file
13
src/Logic/IPasswordStoreService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Models;
|
||||
|
||||
namespace Logic;
|
||||
|
||||
public interface IPasswordStoreService
|
||||
{
|
||||
IEnumerable<PasswordStore> GetAll();
|
||||
PasswordStore Get(uint ID);
|
||||
void Delete(uint ID);
|
||||
void Delete(PasswordStore item);
|
||||
void Create(PasswordStore item);
|
||||
void Edit(uint ID, PasswordStore newItem);
|
||||
}
|
||||
14
src/Logic/Logic.csproj
Normal file
14
src/Logic/Logic.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Models/Models.csproj" />
|
||||
<ProjectReference Include="../Repository/Repository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
39
src/Logic/PasswordStoreService.cs
Normal file
39
src/Logic/PasswordStoreService.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Models;
|
||||
using Repository;
|
||||
|
||||
namespace Logic;
|
||||
|
||||
public class PasswordStoreService : IPasswordStoreService
|
||||
{
|
||||
private readonly IRepository repository;
|
||||
|
||||
public void Create(PasswordStore item)
|
||||
{
|
||||
Create(item);
|
||||
}
|
||||
|
||||
public void Delete(uint ID)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Delete(PasswordStore item)
|
||||
{
|
||||
Delete(item.ID);
|
||||
}
|
||||
|
||||
public void Edit(uint ID, PasswordStore newItem)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public PasswordStore Get(uint ID)
|
||||
{
|
||||
return repository.Get(ID);
|
||||
}
|
||||
|
||||
public IEnumerable<PasswordStore> GetAll()
|
||||
{
|
||||
return (IEnumerable<PasswordStore>)repository.GetAll();
|
||||
}
|
||||
}
|
||||
9
src/Models/Models.csproj
Normal file
9
src/Models/Models.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
9
src/Models/PasswordStore.cs
Normal file
9
src/Models/PasswordStore.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Models;
|
||||
|
||||
public class PasswordStore
|
||||
{
|
||||
public uint ID { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
}
|
||||
14
src/Repository/IRepository.cs
Normal file
14
src/Repository/IRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections;
|
||||
using Models;
|
||||
|
||||
namespace Repository;
|
||||
|
||||
public interface IRepository
|
||||
{
|
||||
List<PasswordStore> GetAll();
|
||||
PasswordStore? Get(uint id);
|
||||
void Edit(uint ID, PasswordStore newItem);
|
||||
void Create(PasswordStore item);
|
||||
void Delete(uint ID);
|
||||
void Delete(PasswordStore item);
|
||||
}
|
||||
172
src/Repository/JsonRepository.cs
Normal file
172
src/Repository/JsonRepository.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System.Text.Json;
|
||||
using Models;
|
||||
|
||||
namespace Repository;
|
||||
|
||||
public class JsonRepository : IRepository, IDisposable
|
||||
{
|
||||
private const string _appName = "Keychain";
|
||||
|
||||
private uint _autoIncrementedId;
|
||||
private readonly string _filePath;
|
||||
private List<PasswordStore> _cache;
|
||||
private bool _cacheAhead;
|
||||
|
||||
public JsonRepository(string fileName)
|
||||
{
|
||||
string? xdgDataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||
string dataHome;
|
||||
if (!string.IsNullOrEmpty(xdgDataHome))
|
||||
{
|
||||
dataHome = Path.Combine(xdgDataHome, _appName);
|
||||
}
|
||||
else
|
||||
{
|
||||
dataHome = Path.Combine(
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share"),
|
||||
_appName
|
||||
);
|
||||
}
|
||||
_filePath = Path.Combine(dataHome, fileName);
|
||||
|
||||
ReadAllFromFile();
|
||||
|
||||
var lastItem = _cache.OrderBy(item => item.ID).LastOrDefault();
|
||||
_autoIncrementedId = lastItem != null ? lastItem.ID : 0;
|
||||
}
|
||||
|
||||
private void ReadAllFromFile()
|
||||
{
|
||||
var items = new List<PasswordStore>();
|
||||
|
||||
if (File.Exists(_filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(_filePath);
|
||||
items = JsonSerializer.Deserialize<List<PasswordStore>>(json) ?? new List<PasswordStore>();
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
WriteToStdErr($"JSON error: {e.Message}");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
WriteToStdErr($"File I/O error: {e.Message}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
WriteToStdErr($"Unexpected error: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_cache = items;
|
||||
_cacheAhead = false;
|
||||
}
|
||||
|
||||
public List<PasswordStore> GetAll()
|
||||
{
|
||||
return _cache;
|
||||
}
|
||||
|
||||
public PasswordStore? Get(uint id)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _cache.First(item => item.ID.Equals(id));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
WriteToStdErr($"Unexpected error: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_cacheAhead)
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = JsonSerializer.Serialize(_cache);
|
||||
string? directory = Path.GetDirectoryName(_filePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory!);
|
||||
}
|
||||
File.WriteAllText(_filePath, json);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
WriteToStdErr($"File I/O error: {e.Message}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
WriteToStdErr($"Unexpected error: {e.Message}");
|
||||
}
|
||||
|
||||
ReadAllFromFile();
|
||||
}
|
||||
}
|
||||
|
||||
public void Edit(uint ID, PasswordStore newItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordStore item = _cache.First(item => item.ID.Equals(ID));
|
||||
item.DisplayName = newItem.DisplayName;
|
||||
item.IconName = newItem.IconName;
|
||||
item.Path = newItem.Path;
|
||||
_cacheAhead = true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
WriteToStdErr($"Edit error: Item with ID {ID} not found.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
WriteToStdErr($"Unexpected error: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Create(PasswordStore item)
|
||||
{
|
||||
item.ID = ++_autoIncrementedId;
|
||||
_cache.Add(item);
|
||||
_cacheAhead = true;
|
||||
}
|
||||
|
||||
public void Delete(PasswordStore item)
|
||||
{
|
||||
Delete(item.ID);
|
||||
}
|
||||
|
||||
public void Delete(uint id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _cache.First(item => item.ID.Equals(id));
|
||||
_cache.Remove(item);
|
||||
_cacheAhead = true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
WriteToStdErr($"Delete error: Item with ID {id} not found.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
WriteToStdErr($"Unexpected error: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteToStdErr(string message)
|
||||
{
|
||||
using var sw = new StreamWriter(Console.OpenStandardError());
|
||||
sw.WriteLine(message);
|
||||
}
|
||||
}
|
||||
13
src/Repository/Repository.csproj
Normal file
13
src/Repository/Repository.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Models/Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user