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

40
src/Keychain.sln Normal file
View 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

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

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

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

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

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

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

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