Moved source to subfolder

This commit is contained in:
2025-09-26 11:04:03 +02:00
parent 22ad11add1
commit 6902aeb9c0
17 changed files with 0 additions and 0 deletions

25
src/App/Keychain.csproj Normal file
View 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
View 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();
}
}

View 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;
}
}
}

View 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>

View 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");
}
}
}

View 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>

View 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);
}
}

View 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;
}
}