Files
aliasvault/apps/server/AliasVault.Client/Main/Components/ClipboardCountdownBar.razor
2025-10-31 22:52:37 +01:00

161 lines
5.0 KiB
Plaintext

@inject ClipboardCopyService ClipboardCopyService
@inject JsInteropService JsInteropService
@inject IStringLocalizerFactory LocalizerFactory
@using Microsoft.Extensions.Localization
@implements IAsyncDisposable
@using Microsoft.JSInterop
@if (IsVisible)
{
<div class="fixed top-0 left-0 right-0 z-50 h-1 bg-gray-200 dark:bg-gray-700">
<div class="h-full @GetBarColorClass() transition-all duration-100 ease-linear @GetAnimationClass()"
style="width: @(Progress * 100)%">
</div>
@if (CurrentStatus == "manual_clear_required")
{
<div class="fixed top-2 left-1/2 transform -translate-x-1/2 z-50 bg-blue-500 dark:bg-blue-600 text-white px-4 py-2 rounded-md shadow-lg flex items-center gap-3">
<button @onclick="HandleManualClear"
class="px-3 py-1 bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 rounded text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700">
@Localizer["ClearClipboardButton"]
</button>
</div>
}
</div>
@if (CurrentStatus == "pending" || CurrentStatus == "manual_clear_required")
{
<style>
@@keyframes custom-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.custom-pulse-animation {
animation: custom-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>
}
}
@code {
private bool IsVisible { get; set; }
private double Progress { get; set; }
private string CurrentStatus { get; set; } = "active";
private DotNetObjectReference<ClipboardCountdownBar>? _objRef;
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.ClipboardCountdownBar", "AliasVault.Client");
/// <summary>
/// Called from JavaScript when clipboard status changes.
/// </summary>
[JSInvokable]
public async Task OnClipboardStatusChange(string status)
{
// Ignore the test 'registered' status
if (status == "registered")
{
return;
}
// Ensure UI updates happen on the UI thread
await InvokeAsync(() =>
{
CurrentStatus = status;
// Hide the bar when clipboard is cleared
if (status == "cleared")
{
IsVisible = false;
Progress = 0;
}
// Update pending state properly
else if (status == "pending")
{
IsVisible = true;
Progress = 1.0;
}
// Handle active state
else if (status == "active")
{
// Let timer handle active state, but ensure we're visible if we should be
}
StateHasChanged();
});
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
ClipboardCopyService.OnTimerProgress -= HandleTimerProgress;
if (_objRef != null)
{
await JsInteropService.UnregisterClipboardStatusCallback();
_objRef.Dispose();
_objRef = null;
}
}
/// <inheritdoc />
protected override async void OnInitialized()
{
ClipboardCopyService.OnTimerProgress += HandleTimerProgress;
// Register for JavaScript status callbacks
_objRef = DotNetObjectReference.Create(this);
await JsInteropService.RegisterClipboardStatusCallback(_objRef);
}
private void HandleTimerProgress(double progress)
{
Progress = progress;
if (progress > 0)
{
// Normal countdown in progress
IsVisible = true;
CurrentStatus = "active";
}
else
{
// Countdown completed - only default to pending if not already cleared
if (CurrentStatus != "cleared")
{
IsVisible = true;
Progress = 1.0; // Show full bar when pending
CurrentStatus = "pending";
}
}
InvokeAsync(StateHasChanged);
}
/// <summary>
/// Handles the manual clear of the clipboard.
/// </summary>
private async Task HandleManualClear()
{
await JsInteropService.ClearClipboard();
CurrentStatus = "active";
IsVisible = true;
Progress = 0;
StateHasChanged();
}
private string GetBarColorClass()
{
return CurrentStatus switch
{
"pending" => "bg-blue-500 dark:bg-blue-400", // Blue when pending (waiting for focus)
"manual_clear_required" => "bg-blue-500 dark:bg-blue-400", // Blue when manual clear required
_ => "bg-orange-500 dark:bg-orange-400" // Orange when actively counting down or cleared
};
}
private string GetAnimationClass()
{
// Add pulse animation when pending to draw attention
return CurrentStatus == "pending" ? "custom-pulse-animation" : "";
}
}