diff --git a/src/AliasGenerators/Identity/Implementations/Base/IdentityGenerator.cs b/src/AliasGenerators/Identity/Implementations/Base/IdentityGenerator.cs
index 031172731..9b6f56a44 100644
--- a/src/AliasGenerators/Identity/Implementations/Base/IdentityGenerator.cs
+++ b/src/AliasGenerators/Identity/Implementations/Base/IdentityGenerator.cs
@@ -12,8 +12,8 @@ using AliasGenerators.Identity;
using AliasGenerators.Identity.Models;
///
-/// Dutch identity generator which implements IIdentityGenerator and generates
-/// random dutch identities.
+/// Abstract identity generator which implements IIdentityGenerator and generates
+/// random identities for a certain language.
///
public abstract class IdentityGenerator : IIdentityGenerator
{
diff --git a/src/AliasVault.Admin/Main/Components/Alerts/GlobalNotificationDisplay.razor b/src/AliasVault.Admin/Main/Components/Alerts/GlobalNotificationDisplay.razor
index 4cd4a7b89..a2882da06 100644
--- a/src/AliasVault.Admin/Main/Components/Alerts/GlobalNotificationDisplay.razor
+++ b/src/AliasVault.Admin/Main/Components/Alerts/GlobalNotificationDisplay.razor
@@ -26,7 +26,7 @@
if (firstRender)
{
- // We subscribe to the OnChange event of the PortalMessageService to update the UI when a new message is added
+ // We subscribe to the OnChange event of the GlobalNotificationService to update the UI when a new message is added
RefreshAddMessages();
GlobalNotificationService.OnChange += RefreshAddMessages;
_onChangeSubscribed = true;
diff --git a/src/AliasVault.Client/Main/Components/Forms/EditFormRow.razor b/src/AliasVault.Client/Main/Components/Forms/EditFormRow.razor
index ebfeccdc2..9f1ee164d 100644
--- a/src/AliasVault.Client/Main/Components/Forms/EditFormRow.razor
+++ b/src/AliasVault.Client/Main/Components/Forms/EditFormRow.razor
@@ -6,7 +6,7 @@
}
else
{
-
+
}
@@ -35,6 +35,12 @@
[Parameter]
public string Value { get; set; } = string.Empty;
+ ///
+ /// Callback that is triggered when the value changes.
+ ///
+ [Parameter]
+ public EventCallback OnFocus { get; set; }
+
///
/// Callback that is triggered when the value changes.
///
@@ -46,4 +52,12 @@
Value = e.Value?.ToString() ?? string.Empty;
await ValueChanged.InvokeAsync(Value);
}
+
+ private async Task OnFocusEvent(FocusEventArgs e)
+ {
+ if (OnFocus.HasDelegate)
+ {
+ await OnFocus.InvokeAsync(e);
+ }
+ }
}
diff --git a/src/AliasVault.Client/Main/Components/Layout/ClickOutsideHandler.razor b/src/AliasVault.Client/Main/Components/Layout/ClickOutsideHandler.razor
index 60644e7f1..9f0b596ad 100644
--- a/src/AliasVault.Client/Main/Components/Layout/ClickOutsideHandler.razor
+++ b/src/AliasVault.Client/Main/Components/Layout/ClickOutsideHandler.razor
@@ -55,7 +55,7 @@
///
public async ValueTask DisposeAsync()
{
- if (Module != null)
+ if (Module is not null)
{
await Module.InvokeVoidAsync("unregisterClickOutsideHandler");
await Module.DisposeAsync();
@@ -69,9 +69,9 @@
///
private async Task LoadModuleAsync()
{
- if (Module == null)
+ if (Module is null)
{
- Module = await JSRuntime.InvokeAsync("import", "./js/clickOutsideHandler.js");
+ Module = await JSRuntime.InvokeAsync("import", "./js/modules/clickOutsideHandler.js");
ObjRef = DotNetObjectReference.Create(this);
}
}
diff --git a/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor b/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor
new file mode 100644
index 000000000..577601d4c
--- /dev/null
+++ b/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor
@@ -0,0 +1,210 @@
+@using System.ComponentModel.DataAnnotations
+@inherits AliasVault.Client.Main.Pages.MainBase
+@inject IJSRuntime JSRuntime
+@inject CredentialService CredentialService
+@implements IAsyncDisposable
+
+
+
+@if (IsPopupVisible)
+{
+
+
+
Create New Identity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+@code {
+ private const string DefaultServiceUrl = "https://";
+ private bool IsPopupVisible = false;
+ private bool IsCreating = false;
+ private CreateModel Model = new();
+ private string PopupStyle { get; set; } = "";
+ private ElementReference buttonRef;
+ private IJSObjectReference? Module;
+
+ ///
+ async ValueTask IAsyncDisposable.DisposeAsync()
+ {
+ await KeyboardShortcutService.UnregisterShortcutAsync("gc");
+ if (Module is not null)
+ {
+ await Module.DisposeAsync();
+ }
+ }
+
+ ///
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ await KeyboardShortcutService.RegisterShortcutAsync("gc", ShowPopup);
+ Module = await JSRuntime.InvokeAsync("import", "./js/modules/newIdentityWidget.js");
+ }
+ }
+
+ ///
+ /// When the URL input is focused, place cursor at the end of the default URL to allow for easy typing.
+ ///
+ private void OnFocusUrlInput(FocusEventArgs e)
+ {
+ if (Model.ServiceUrl != DefaultServiceUrl)
+ {
+ return;
+ }
+
+ // Use a small delay to ensure the focus is set after the browser's default behavior.
+ Task.Delay(1).ContinueWith(_ =>
+ {
+ JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('serviceUrl').setSelectionRange({DefaultServiceUrl.Length}, {DefaultServiceUrl.Length})");
+ });
+ }
+
+ private async Task TogglePopup()
+ {
+ IsPopupVisible = !IsPopupVisible;
+ if (IsPopupVisible)
+ {
+ await ShowPopup();
+ }
+ }
+
+ private async Task ShowPopup()
+ {
+ IsPopupVisible = true;
+
+ // Clear the input fields
+ Model = new();
+ Model.ServiceUrl = DefaultServiceUrl;
+
+ await UpdatePopupStyle();
+ await Task.Delay(100); // Give time for the DOM to update
+ await JSRuntime.InvokeVoidAsync("focusElement", "serviceName");
+ }
+
+ private void ClosePopup()
+ {
+ IsPopupVisible = false;
+ }
+
+ private async Task UpdatePopupStyle()
+ {
+ var windowWidth = await JSRuntime.InvokeAsync("getWindowWidth");
+ var buttonRect = await JSRuntime.InvokeAsync("getElementRect", buttonRef);
+
+ var popupWidth = Math.Min(400, windowWidth - 20); // 20px for some padding
+ var leftPosition = Math.Max(0, Math.Min(buttonRect.Left, windowWidth - popupWidth - 10));
+
+ PopupStyle = $"width: {popupWidth}px; left: {leftPosition}px; top: {buttonRect.Bottom}px;";
+ StateHasChanged();
+ }
+
+ private async Task CreateIdentity()
+ {
+ if (IsCreating)
+ {
+ return;
+ }
+
+ IsCreating = true;
+ GlobalLoadingSpinner.Show();
+ StateHasChanged();
+
+ var credential = new Credential();
+ credential.Alias = new Alias();
+ credential.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
+ credential.Service = new Service();
+ credential.Service.Name = Model.ServiceName;
+
+ if (Model.ServiceUrl != DefaultServiceUrl)
+ {
+ credential.Service.Url = Model.ServiceUrl;
+ }
+
+ credential.Passwords = new List { new() };
+ await CredentialService.GenerateRandomIdentity(credential);
+
+ var id = await CredentialService.InsertEntryAsync(credential);
+ if (id == Guid.Empty)
+ {
+ // Error saving.
+ IsCreating = false;
+ GlobalLoadingSpinner.Hide();
+ GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true);
+ return;
+ }
+
+ // No error, add success message.
+ GlobalNotificationService.AddSuccessMessage("Credentials created successfully.");
+
+ NavigationManager.NavigateTo("/credentials/" + id);
+
+ IsCreating = false;
+ GlobalLoadingSpinner.Hide();
+ StateHasChanged();
+ ClosePopup();
+ }
+
+ private void OpenAdvancedMode()
+ {
+ NavigationManager.NavigateTo("/credentials/create");
+ ClosePopup();
+ }
+
+ ///
+ /// Bounding client rectangle returned from JavaScript.
+ ///
+ private sealed class BoundingClientRect
+ {
+ public double Left { get; set; }
+ public double Top { get; set; }
+ public double Right { get; set; }
+ public double Bottom { get; set; }
+ public double Width { get; set; }
+ public double Height { get; set; }
+ }
+
+ ///
+ /// Local model for the form with support for validation.
+ ///
+ private sealed class CreateModel
+ {
+ ///
+ /// The service name.
+ ///
+ [Required]
+ [Display(Name = "Service Name")]
+ public string ServiceName { get; set; } = string.Empty;
+
+ ///
+ /// The service URL.
+ ///
+ [Display(Name = "Service URL")]
+ public string ServiceUrl { get; set; } = string.Empty;
+ }
+}
diff --git a/src/AliasVault.Client/Main/Layout/DbStatusIndicator.razor b/src/AliasVault.Client/Main/Layout/DbStatusIndicator.razor
index 3058bc2ad..790ed7782 100644
--- a/src/AliasVault.Client/Main/Layout/DbStatusIndicator.razor
+++ b/src/AliasVault.Client/Main/Layout/DbStatusIndicator.razor
@@ -1,16 +1,18 @@
@implements IDisposable
@inject DbService DbService
-@if (Loading)
-{
-
-
-
-}
-else
-{
-
-}
+
+ @if (Loading)
+ {
+
+
+
+ }
+ else
+ {
+
+ }
+