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 + { + + } +